Compare commits
60 Commits
@nhost/vue
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5ff53c622 | ||
|
|
d21714d169 | ||
|
|
0d16ad41b8 | ||
|
|
82c328eeda | ||
|
|
d991cd8c7e | ||
|
|
e469628ebe | ||
|
|
856bc0a4bb | ||
|
|
9b1fb1ce28 | ||
|
|
a4d16f1835 | ||
|
|
3db8644075 | ||
|
|
7f667f6acb | ||
|
|
685dc6c1e4 | ||
|
|
6f7f2b0a65 | ||
|
|
6d0167b33f | ||
|
|
3ffb60f0ae | ||
|
|
97ced73a3c | ||
|
|
39c86cea25 | ||
|
|
d2d590db7e | ||
|
|
3bdbefc015 | ||
|
|
79081b43c2 | ||
|
|
a4b541f100 | ||
|
|
4523020c33 | ||
|
|
2e2248fd44 | ||
|
|
63358eb80b | ||
|
|
ded674fab6 | ||
|
|
85f2f28902 | ||
|
|
b8e9ad831e | ||
|
|
4e0c5dd1d3 | ||
|
|
b874109c6d | ||
|
|
21b926cc07 | ||
|
|
c35cd47d97 | ||
|
|
8dcd801c7c | ||
|
|
e3199be749 | ||
|
|
284b31e036 | ||
|
|
e7593c7de8 | ||
|
|
e6d862ac1b | ||
|
|
f73672372f | ||
|
|
7f12b98d94 | ||
|
|
d79b66314d | ||
|
|
2a58266592 | ||
|
|
44c2c5467d | ||
|
|
142752cb79 | ||
|
|
b05236a23c | ||
|
|
11a46a0db1 | ||
|
|
cedff501d6 | ||
|
|
7c426dafb2 | ||
|
|
57e7f794f5 | ||
|
|
d4b6cb0acf | ||
|
|
5d0cf8814b | ||
|
|
96cf17bbeb | ||
|
|
ed1a8d458e | ||
|
|
8077495c18 | ||
|
|
b617ec7186 | ||
|
|
bb2da11dd4 | ||
|
|
94fa824e7d | ||
|
|
32d1ee124f | ||
|
|
138bf9eb5a | ||
|
|
d8d9310e0b | ||
|
|
67b2c044b8 | ||
|
|
0b7790ca83 |
@@ -1,5 +1,18 @@
|
|||||||
# @nhost/dashboard
|
# @nhost/dashboard
|
||||||
|
|
||||||
|
## 0.21.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 97ced73a3: fix(dashboard): prevent dashboard from resolving secrets
|
||||||
|
|
||||||
|
## 0.21.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- ed1a8d458: Update alert message on increasing PostgreSQL's volume capacity
|
||||||
|
- 2e2248fd4: feat(dashboard): add SQL editor
|
||||||
|
|
||||||
## 0.20.28
|
## 0.20.28
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -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 add --no-cache libc6-compat
|
||||||
RUN apk update
|
RUN apk update
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -7,7 +7,7 @@ RUN yarn global add turbo@1.10.11
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||||
|
|
||||||
FROM node:16-alpine AS builder
|
FROM node:18-alpine AS builder
|
||||||
ARG TURBO_TOKEN
|
ARG TURBO_TOKEN
|
||||||
ARG TURBO_TEAM
|
ARG TURBO_TEAM
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ COPY turbo.json turbo.json
|
|||||||
COPY config/ config/
|
COPY config/ config/
|
||||||
RUN pnpm build:dashboard
|
RUN pnpm build:dashboard
|
||||||
|
|
||||||
FROM node:16-alpine AS runner
|
FROM node:18-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/dashboard",
|
"name": "@nhost/dashboard",
|
||||||
"version": "0.20.28",
|
"version": "0.21.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.7.10",
|
"@apollo/client": "^3.7.10",
|
||||||
"@codemirror/language": "^6.3.0",
|
"@codemirror/lang-sql": "^6.5.4",
|
||||||
"@emotion/cache": "^11.10.5",
|
"@emotion/cache": "^11.10.5",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/server": "^11.4.0",
|
"@emotion/server": "^11.4.0",
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
"@tanstack/react-query": "^4.16.1",
|
"@tanstack/react-query": "^4.16.1",
|
||||||
"@tanstack/react-table": "^8.5.30",
|
"@tanstack/react-table": "^8.5.30",
|
||||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
"@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",
|
"analytics-node": "^6.2.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
"react-is": "18.2.0",
|
"react-is": "18.2.0",
|
||||||
"react-loading-skeleton": "^2.2.0",
|
"react-loading-skeleton": "^2.2.0",
|
||||||
"react-merge-refs": "^1.1.0",
|
"react-merge-refs": "^1.1.0",
|
||||||
|
"react-resizable-layout": "^0.7.2",
|
||||||
"react-syntax-highlighter": "^15.4.5",
|
"react-syntax-highlighter": "^15.4.5",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
"sharp": "^0.32.0",
|
"sharp": "^0.32.0",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as TerminalIcon } from './TerminalIcon';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
query GetAuthenticationSettings($appId: uuid!) {
|
query GetAuthenticationSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
auth {
|
auth {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon'
|
|||||||
import { LockIcon } from '@/components/ui/v2/icons/LockIcon';
|
import { LockIcon } from '@/components/ui/v2/icons/LockIcon';
|
||||||
import { PencilIcon } from '@/components/ui/v2/icons/PencilIcon';
|
import { PencilIcon } from '@/components/ui/v2/icons/PencilIcon';
|
||||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
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 { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||||
import { UsersIcon } from '@/components/ui/v2/icons/UsersIcon';
|
import { UsersIcon } from '@/components/ui/v2/icons/UsersIcon';
|
||||||
import { Link } from '@/components/ui/v2/Link';
|
import { Link } from '@/components/ui/v2/Link';
|
||||||
@@ -86,7 +87,9 @@ function DataBrowserSidebarContent({
|
|||||||
const isGitHubConnected = !!currentProject?.githubRepository;
|
const isGitHubConnected = !!currentProject?.githubRepository;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
asPath,
|
||||||
query: { workspaceSlug, appSlug, dataSourceSlug, schemaSlug, tableSlug },
|
query: { workspaceSlug, appSlug, dataSourceSlug, schemaSlug, tableSlug },
|
||||||
} = router;
|
} = router;
|
||||||
|
|
||||||
@@ -108,6 +111,8 @@ function DataBrowserSidebarContent({
|
|||||||
*/
|
*/
|
||||||
const [sidebarMenuTable, setSidebarMenuTable] = useState<string>();
|
const [sidebarMenuTable, setSidebarMenuTable] = useState<string>();
|
||||||
|
|
||||||
|
const sqlEditorHref = `/${workspaceSlug}/${appSlug}/database/browser/default/editor`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedSchema) {
|
if (selectedSchema) {
|
||||||
return;
|
return;
|
||||||
@@ -258,194 +263,135 @@ function DataBrowserSidebarContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-1">
|
<Box className="flex h-full flex-col justify-between">
|
||||||
{schemas && schemas.length > 0 && (
|
<Box className="flex flex-col px-2">
|
||||||
<Select
|
{schemas && schemas.length > 0 && (
|
||||||
renderValue={(option) => (
|
<Select
|
||||||
<span className="grid grid-flow-col items-center gap-1">
|
renderValue={(option) => (
|
||||||
{option?.label}
|
<span className="grid grid-flow-col items-center gap-1">
|
||||||
</span>
|
{option?.label}
|
||||||
)}
|
</span>
|
||||||
slotProps={{
|
)}
|
||||||
listbox: { className: 'max-w-[220px] min-w-[initial] w-full' },
|
slotProps={{
|
||||||
popper: { className: 'max-w-[220px] min-w-[initial] w-full' },
|
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)}
|
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"
|
|
||||||
>
|
>
|
||||||
Learn More <ArrowRightIcon />
|
{schemas.map((schema) => (
|
||||||
</Link>
|
<Option
|
||||||
</Box>
|
className="grid grid-flow-col items-center gap-1"
|
||||||
)}
|
value={schema.schema_name}
|
||||||
|
key={schema.schema_name}
|
||||||
{!isSelectedSchemaLocked && (
|
>
|
||||||
<Button
|
<Text className="text-sm">
|
||||||
variant="borderless"
|
<Text component="span" color="disabled">
|
||||||
endIcon={<PlusIcon />}
|
schema.
|
||||||
className="mt-1 w-full justify-between px-2"
|
</Text>
|
||||||
onClick={() => {
|
<Text component="span" className="font-medium">
|
||||||
openDrawer({
|
{schema.schema_name}
|
||||||
title: 'Create a New Table',
|
</Text>
|
||||||
component: (
|
</Text>
|
||||||
<CreateTableForm onSubmit={refetch} schema={selectedSchema} />
|
{(isSchemaLocked(schema.schema_name) || isGitHubConnected) && (
|
||||||
),
|
<LockIcon
|
||||||
});
|
className="h-3 w-3"
|
||||||
|
sx={{ color: 'text.secondary' }}
|
||||||
onSidebarItemClick();
|
/>
|
||||||
}}
|
)}
|
||||||
disabled={isGitHubConnected}
|
</Option>
|
||||||
>
|
))}
|
||||||
New Table
|
</Select>
|
||||||
</Button>
|
)}
|
||||||
)}
|
{isGitHubConnected && (
|
||||||
|
<Box
|
||||||
{schemas && schemas.length > 0 && tablesInSelectedSchema.length === 0 && (
|
className="mt-1.5 grid grid-flow-row justify-items-start gap-2 rounded-md p-2"
|
||||||
<Text className="py-1.5 px-2 text-xs" color="disabled">
|
sx={{ backgroundColor: 'grey.200' }}
|
||||||
No tables found.
|
>
|
||||||
</Text>
|
<Text>
|
||||||
)}
|
Your project is connected to GitHub. Please use the CLI to make
|
||||||
|
schema changes.
|
||||||
<nav aria-label="Database navigation">
|
</Text>
|
||||||
{tablesInSelectedSchema.length > 0 && (
|
<Link
|
||||||
<List className="grid gap-1 pb-6">
|
href="https://docs.nhost.io/platform/github-integration"
|
||||||
{tablesInSelectedSchema.map((table) => {
|
target="_blank"
|
||||||
const tablePath = `${table.table_schema}.${table.table_name}`;
|
rel="noopener noreferrer"
|
||||||
const isSelected = `${schemaSlug}.${tableSlug}` === tablePath;
|
underline="hover"
|
||||||
const isSidebarMenuOpen = sidebarMenuTable === tablePath;
|
className="grid grid-flow-col items-center justify-start gap-1"
|
||||||
|
>
|
||||||
return (
|
Learn More <ArrowRightIcon />
|
||||||
<ListItem.Root
|
</Link>
|
||||||
className="group"
|
</Box>
|
||||||
key={tablePath}
|
)}
|
||||||
secondaryAction={
|
{!isSelectedSchemaLocked && (
|
||||||
<Dropdown.Root
|
<Button
|
||||||
id="table-management-menu"
|
variant="borderless"
|
||||||
onOpen={() => setSidebarMenuTable(tablePath)}
|
endIcon={<PlusIcon />}
|
||||||
onClose={() => setSidebarMenuTable(undefined)}
|
className="mt-1 w-full justify-between px-2"
|
||||||
>
|
onClick={() => {
|
||||||
<Dropdown.Trigger
|
openDrawer({
|
||||||
asChild
|
title: 'Create a New Table',
|
||||||
hideChevron
|
component: (
|
||||||
disabled={tablePath === removableTable}
|
<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
|
<Dropdown.Trigger
|
||||||
variant="borderless"
|
asChild
|
||||||
color={isSelected ? 'primary' : 'secondary'}
|
hideChevron
|
||||||
className={twMerge(
|
disabled={tablePath === removableTable}
|
||||||
!isSelected &&
|
|
||||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<DotsHorizontalIcon />
|
<IconButton
|
||||||
</IconButton>
|
variant="borderless"
|
||||||
</Dropdown.Trigger>
|
color={isSelected ? 'primary' : 'secondary'}
|
||||||
|
className={twMerge(
|
||||||
<Dropdown.Content menu PaperProps={{ className: 'w-52' }}>
|
!isSelected &&
|
||||||
{isGitHubConnected ? (
|
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
||||||
<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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<UsersIcon
|
<DotsHorizontalIcon />
|
||||||
className="h-4 w-4"
|
</IconButton>
|
||||||
sx={{ color: 'text.secondary' }}
|
</Dropdown.Trigger>
|
||||||
/>
|
<Dropdown.Content
|
||||||
|
menu
|
||||||
<span>View Permissions</span>
|
PaperProps={{ className: 'w-52' }}
|
||||||
</Dropdown.Item>
|
>
|
||||||
) : (
|
{isGitHubConnected ? (
|
||||||
[
|
|
||||||
!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
|
<Dropdown.Item
|
||||||
key="edit-permissions"
|
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleEditPermissionClick(
|
handleEditPermissionClick(
|
||||||
table.table_schema,
|
table.table_schema,
|
||||||
table.table_name,
|
table.table_name,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -453,68 +399,135 @@ function DataBrowserSidebarContent({
|
|||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
sx={{ color: 'text.secondary' }}
|
sx={{ color: 'text.secondary' }}
|
||||||
/>
|
/>
|
||||||
|
<span>View Permissions</span>
|
||||||
<span>Edit Permissions</span>
|
</Dropdown.Item>
|
||||||
</Dropdown.Item>,
|
) : (
|
||||||
!isSelectedSchemaLocked && (
|
[
|
||||||
<Divider
|
!isSelectedSchemaLocked && (
|
||||||
key="edit-permissions-separator"
|
<Dropdown.Item
|
||||||
component="li"
|
key="edit-table"
|
||||||
/>
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
),
|
onClick={() =>
|
||||||
!isSelectedSchemaLocked && (
|
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
|
<Dropdown.Item
|
||||||
key="delete-table"
|
key="edit-permissions"
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
sx={{ color: 'error.main' }}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleDeleteTableClick(
|
handleEditPermissionClick(
|
||||||
table.table_schema,
|
table.table_schema,
|
||||||
table.table_name,
|
table.table_name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TrashIcon
|
<UsersIcon
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
sx={{ color: 'error.main' }}
|
sx={{ color: 'text.secondary' }}
|
||||||
/>
|
/>
|
||||||
|
<span>Edit Permissions</span>
|
||||||
<span>Delete Table</span>
|
</Dropdown.Item>,
|
||||||
</Dropdown.Item>
|
!isSelectedSchemaLocked && (
|
||||||
),
|
<Divider
|
||||||
]
|
key="edit-permissions-separator"
|
||||||
)}
|
component="li"
|
||||||
</Dropdown.Content>
|
/>
|
||||||
</Dropdown.Root>
|
),
|
||||||
}
|
!isSelectedSchemaLocked && (
|
||||||
>
|
<Dropdown.Item
|
||||||
<ListItem.Button
|
key="delete-table"
|
||||||
dense
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
selected={isSelected}
|
sx={{ color: 'error.main' }}
|
||||||
disabled={tablePath === removableTable}
|
onClick={() =>
|
||||||
className="group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
|
handleDeleteTableClick(
|
||||||
sx={{
|
table.table_schema,
|
||||||
paddingRight:
|
table.table_name,
|
||||||
(isSelected || isSidebarMenuOpen) &&
|
)
|
||||||
'2.25rem !important',
|
}
|
||||||
}}
|
>
|
||||||
component={NavLink}
|
<TrashIcon
|
||||||
href={`/${workspaceSlug}/${appSlug}/database/browser/default/${table.table_schema}/${table.table_name}`}
|
className="h-4 w-4"
|
||||||
onClick={() => {
|
sx={{ color: 'error.main' }}
|
||||||
if (onSidebarItemClick) {
|
/>
|
||||||
onSidebarItemClick(`default.${tablePath}`);
|
<span>Delete Table</span>
|
||||||
}
|
</Dropdown.Item>
|
||||||
}}
|
),
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
</Dropdown.Content>
|
||||||
|
</Dropdown.Root>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ListItem.Text>{table.table_name}</ListItem.Text>
|
<ListItem.Button
|
||||||
</ListItem.Button>
|
dense
|
||||||
</ListItem.Root>
|
selected={isSelected}
|
||||||
);
|
disabled={tablePath === removableTable}
|
||||||
})}
|
className="group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
|
||||||
</List>
|
sx={{
|
||||||
)}
|
paddingRight:
|
||||||
</nav>
|
(isSelected || isSidebarMenuOpen) &&
|
||||||
</div>
|
'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
|
<Box
|
||||||
component="aside"
|
component="aside"
|
||||||
className={twMerge(
|
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',
|
expanded ? 'translate-x-0' : '-translate-x-full sm:translate-x-0',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as SQLEditor } from './SQLEditor';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as useRunSQL } from './useRunSQL';
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ import { useUI } from '@/components/common/UIProvider';
|
|||||||
import { Form } from '@/components/form/Form';
|
import { Form } from '@/components/form/Form';
|
||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
|
import { Alert } from '@/components/ui/v2/Alert';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { Box } from '@/components/ui/v2/Box';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
|
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
|
||||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||||
import {
|
import {
|
||||||
@@ -144,15 +144,11 @@ export default function AuthDomain() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{!currentProject.plan.isFree && (
|
{!currentProject.plan.isFree && (
|
||||||
<Box
|
<Alert severity="info" className="col-span-6 text-left">
|
||||||
className="grid items-center grid-flow-col gap-1 p-3 rounded-lg shadow-sm place-content-between"
|
Note that volumes can only be increased (not decreased). Also, due
|
||||||
sx={{ backgroundColor: 'grey.200' }}
|
to an AWS limitation, the same volume can only be increased once
|
||||||
>
|
every 6 hours.
|
||||||
<Text>
|
</Alert>
|
||||||
⚠️ Please note that once you increase the storage, it cannot be
|
|
||||||
reduced.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ query GetPostgresSettings($appId: uuid!) {
|
|||||||
database
|
database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
postgres {
|
postgres {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query GetHasuraSettings($appId: uuid!) {
|
query GetHasuraSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
hasura {
|
hasura {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ fragment ServiceResources on ConfigConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query GetResources($appId: uuid!) {
|
query GetResources($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
...ServiceResources
|
...ServiceResources
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query GetStorageSettings($appId: uuid!) {
|
query GetStorageSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
storage {
|
storage {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query getProjectLocales($appId: uuid!) {
|
query getProjectLocales($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
auth {
|
auth {
|
||||||
user {
|
user {
|
||||||
locale {
|
locale {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fragment JWTSecret on ConfigJWTSecret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query GetEnvironmentVariables($appId: uuid!) {
|
query GetEnvironmentVariables($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
global {
|
global {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ fragment PermissionVariable on ConfigAuthsessionaccessTokenCustomClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query GetRolesPermissions($appId: uuid!) {
|
query GetRolesPermissions($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
auth {
|
auth {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query GetSignInMethods($appId: uuid!) {
|
query GetSignInMethods($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
provider {
|
provider {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query GetSmtpSettings($appId: uuid!) {
|
query GetSmtpSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
provider {
|
provider {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
};
|
||||||
51
dashboard/src/utils/__generated__/graphql.ts
generated
51
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -655,7 +655,9 @@ export type ConfigAuthSessionUpdateInput = {
|
|||||||
|
|
||||||
export type ConfigAuthSignUp = {
|
export type ConfigAuthSignUp = {
|
||||||
__typename?: 'ConfigAuthSignUp';
|
__typename?: 'ConfigAuthSignUp';
|
||||||
/** Inverse of AUTH_DISABLE_NEW_USERS */
|
/** AUTH_DISABLE_NEW_USERS */
|
||||||
|
disableNewUsers?: Maybe<Scalars['Boolean']>;
|
||||||
|
/** Inverse of AUTH_DISABLE_SIGNUP */
|
||||||
enabled?: Maybe<Scalars['Boolean']>;
|
enabled?: Maybe<Scalars['Boolean']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -663,14 +665,17 @@ export type ConfigAuthSignUpComparisonExp = {
|
|||||||
_and?: InputMaybe<Array<ConfigAuthSignUpComparisonExp>>;
|
_and?: InputMaybe<Array<ConfigAuthSignUpComparisonExp>>;
|
||||||
_not?: InputMaybe<ConfigAuthSignUpComparisonExp>;
|
_not?: InputMaybe<ConfigAuthSignUpComparisonExp>;
|
||||||
_or?: InputMaybe<Array<ConfigAuthSignUpComparisonExp>>;
|
_or?: InputMaybe<Array<ConfigAuthSignUpComparisonExp>>;
|
||||||
|
disableNewUsers?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigAuthSignUpInsertInput = {
|
export type ConfigAuthSignUpInsertInput = {
|
||||||
|
disableNewUsers?: InputMaybe<Scalars['Boolean']>;
|
||||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigAuthSignUpUpdateInput = {
|
export type ConfigAuthSignUpUpdateInput = {
|
||||||
|
disableNewUsers?: InputMaybe<Scalars['Boolean']>;
|
||||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1042,6 +1047,7 @@ export type ConfigFloatComparisonExp = {
|
|||||||
export type ConfigFunctions = {
|
export type ConfigFunctions = {
|
||||||
__typename?: 'ConfigFunctions';
|
__typename?: 'ConfigFunctions';
|
||||||
node?: Maybe<ConfigFunctionsNode>;
|
node?: Maybe<ConfigFunctionsNode>;
|
||||||
|
resources?: Maybe<ConfigFunctionsResources>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigFunctionsComparisonExp = {
|
export type ConfigFunctionsComparisonExp = {
|
||||||
@@ -1049,10 +1055,12 @@ export type ConfigFunctionsComparisonExp = {
|
|||||||
_not?: InputMaybe<ConfigFunctionsComparisonExp>;
|
_not?: InputMaybe<ConfigFunctionsComparisonExp>;
|
||||||
_or?: InputMaybe<Array<ConfigFunctionsComparisonExp>>;
|
_or?: InputMaybe<Array<ConfigFunctionsComparisonExp>>;
|
||||||
node?: InputMaybe<ConfigFunctionsNodeComparisonExp>;
|
node?: InputMaybe<ConfigFunctionsNodeComparisonExp>;
|
||||||
|
resources?: InputMaybe<ConfigFunctionsResourcesComparisonExp>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigFunctionsInsertInput = {
|
export type ConfigFunctionsInsertInput = {
|
||||||
node?: InputMaybe<ConfigFunctionsNodeInsertInput>;
|
node?: InputMaybe<ConfigFunctionsNodeInsertInput>;
|
||||||
|
resources?: InputMaybe<ConfigFunctionsResourcesInsertInput>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigFunctionsNode = {
|
export type ConfigFunctionsNode = {
|
||||||
@@ -1075,8 +1083,29 @@ export type ConfigFunctionsNodeUpdateInput = {
|
|||||||
version?: InputMaybe<Scalars['Int']>;
|
version?: InputMaybe<Scalars['Int']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ConfigFunctionsResources = {
|
||||||
|
__typename?: 'ConfigFunctionsResources';
|
||||||
|
networking?: Maybe<ConfigNetworking>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigFunctionsResourcesComparisonExp = {
|
||||||
|
_and?: InputMaybe<Array<ConfigFunctionsResourcesComparisonExp>>;
|
||||||
|
_not?: InputMaybe<ConfigFunctionsResourcesComparisonExp>;
|
||||||
|
_or?: InputMaybe<Array<ConfigFunctionsResourcesComparisonExp>>;
|
||||||
|
networking?: InputMaybe<ConfigNetworkingComparisonExp>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigFunctionsResourcesInsertInput = {
|
||||||
|
networking?: InputMaybe<ConfigNetworkingInsertInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigFunctionsResourcesUpdateInput = {
|
||||||
|
networking?: InputMaybe<ConfigNetworkingUpdateInput>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConfigFunctionsUpdateInput = {
|
export type ConfigFunctionsUpdateInput = {
|
||||||
node?: InputMaybe<ConfigFunctionsNodeUpdateInput>;
|
node?: InputMaybe<ConfigFunctionsNodeUpdateInput>;
|
||||||
|
resources?: InputMaybe<ConfigFunctionsResourcesUpdateInput>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Global configuration that applies to all services */
|
/** Global configuration that applies to all services */
|
||||||
@@ -23309,7 +23338,7 @@ export type DeletePersonalAccessTokenMutationResult = Apollo.MutationResult<Dele
|
|||||||
export type DeletePersonalAccessTokenMutationOptions = Apollo.BaseMutationOptions<DeletePersonalAccessTokenMutation, DeletePersonalAccessTokenMutationVariables>;
|
export type DeletePersonalAccessTokenMutationOptions = Apollo.BaseMutationOptions<DeletePersonalAccessTokenMutation, DeletePersonalAccessTokenMutationVariables>;
|
||||||
export const GetAuthenticationSettingsDocument = gql`
|
export const GetAuthenticationSettingsDocument = gql`
|
||||||
query GetAuthenticationSettings($appId: uuid!) {
|
query GetAuthenticationSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
auth {
|
auth {
|
||||||
@@ -23403,7 +23432,7 @@ export const GetPostgresSettingsDocument = gql`
|
|||||||
database
|
database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
postgres {
|
postgres {
|
||||||
@@ -23482,7 +23511,7 @@ export type ResetDatabasePasswordMutationResult = Apollo.MutationResult<ResetDat
|
|||||||
export type ResetDatabasePasswordMutationOptions = Apollo.BaseMutationOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>;
|
export type ResetDatabasePasswordMutationOptions = Apollo.BaseMutationOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>;
|
||||||
export const GetHasuraSettingsDocument = gql`
|
export const GetHasuraSettingsDocument = gql`
|
||||||
query GetHasuraSettings($appId: uuid!) {
|
query GetHasuraSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
hasura {
|
hasura {
|
||||||
@@ -23630,7 +23659,7 @@ export function refetchGetBackupPresignedUrlQuery(variables: GetBackupPresignedU
|
|||||||
}
|
}
|
||||||
export const GetResourcesDocument = gql`
|
export const GetResourcesDocument = gql`
|
||||||
query GetResources($appId: uuid!) {
|
query GetResources($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
...ServiceResources
|
...ServiceResources
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23668,7 +23697,7 @@ export function refetchGetResourcesQuery(variables: GetResourcesQueryVariables)
|
|||||||
}
|
}
|
||||||
export const GetStorageSettingsDocument = gql`
|
export const GetStorageSettingsDocument = gql`
|
||||||
query GetStorageSettings($appId: uuid!) {
|
query GetStorageSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
storage {
|
storage {
|
||||||
@@ -23914,7 +23943,7 @@ export function refetchGetApplicationStateQuery(variables: GetApplicationStateQu
|
|||||||
}
|
}
|
||||||
export const GetProjectLocalesDocument = gql`
|
export const GetProjectLocalesDocument = gql`
|
||||||
query getProjectLocales($appId: uuid!) {
|
query getProjectLocales($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
auth {
|
auth {
|
||||||
user {
|
user {
|
||||||
locale {
|
locale {
|
||||||
@@ -24260,7 +24289,7 @@ export function refetchDnsLookupCnameQuery(variables: DnsLookupCnameQueryVariabl
|
|||||||
}
|
}
|
||||||
export const GetEnvironmentVariablesDocument = gql`
|
export const GetEnvironmentVariablesDocument = gql`
|
||||||
query GetEnvironmentVariables($appId: uuid!) {
|
query GetEnvironmentVariables($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
global {
|
global {
|
||||||
@@ -24312,7 +24341,7 @@ export function refetchGetEnvironmentVariablesQuery(variables: GetEnvironmentVar
|
|||||||
}
|
}
|
||||||
export const GetRolesPermissionsDocument = gql`
|
export const GetRolesPermissionsDocument = gql`
|
||||||
query GetRolesPermissions($appId: uuid!) {
|
query GetRolesPermissions($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
auth {
|
auth {
|
||||||
@@ -24506,7 +24535,7 @@ export type UpdateSecretMutationResult = Apollo.MutationResult<UpdateSecretMutat
|
|||||||
export type UpdateSecretMutationOptions = Apollo.BaseMutationOptions<UpdateSecretMutation, UpdateSecretMutationVariables>;
|
export type UpdateSecretMutationOptions = Apollo.BaseMutationOptions<UpdateSecretMutation, UpdateSecretMutationVariables>;
|
||||||
export const GetSignInMethodsDocument = gql`
|
export const GetSignInMethodsDocument = gql`
|
||||||
query GetSignInMethods($appId: uuid!) {
|
query GetSignInMethods($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
provider {
|
provider {
|
||||||
@@ -24670,7 +24699,7 @@ export function refetchGetSignInMethodsQuery(variables: GetSignInMethodsQueryVar
|
|||||||
}
|
}
|
||||||
export const GetSmtpSettingsDocument = gql`
|
export const GetSmtpSettingsDocument = gql`
|
||||||
query GetSmtpSettings($appId: uuid!) {
|
query GetSmtpSettings($appId: uuid!) {
|
||||||
config(appID: $appId, resolve: true) {
|
config(appID: $appId, resolve: false) {
|
||||||
id: __typename
|
id: __typename
|
||||||
__typename
|
__typename
|
||||||
provider {
|
provider {
|
||||||
|
|||||||
53
dashboard/src/utils/sql/index.ts
Normal file
53
dashboard/src/utils/sql/index.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
# @nhost/docs
|
# @nhost/docs
|
||||||
|
|
||||||
|
## 0.7.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 3ffb60f0a: fixed typo in Run deploy example script
|
||||||
|
|
||||||
|
## 0.7.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 138bf9eb5: fix: add instructions for enabling Sign In with LinkedIn using OpenID Connect
|
||||||
|
|
||||||
## 0.7.1
|
## 0.7.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ Follow this guide to sign in users with LinkedIn.
|
|||||||
- Copy and paste the **OAuth Callback URL** from Nhost.
|
- Copy and paste the **OAuth Callback URL** from Nhost.
|
||||||
- Click **Update**.
|
- 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
|
## Configure Nhost
|
||||||
|
|
||||||
- Copy and paste the **Client ID** and **Client Secret** from LinkedIn to your Nhost OAuth settings for LinkedIn.
|
- Copy and paste the **Client ID** and **Client Secret** from LinkedIn to your Nhost OAuth settings for LinkedIn.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ docker buildx build \
|
|||||||
.
|
.
|
||||||
|
|
||||||
nhost run config-deploy \
|
nhost run config-deploy \
|
||||||
--config $CONFIGURATION \
|
--config $CONFIGURATION_FILE \
|
||||||
--service-id $SERVICE_ID
|
--service-id $SERVICE_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/docs",
|
"name": "@nhost/docs",
|
||||||
"version": "0.7.1",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['../../config/.eslintrc.js', 'plugin:@next/next/recommended'],
|
extends: ['../../../config/.eslintrc.js', 'plugin:@next/next/recommended'],
|
||||||
rules: {
|
rules: {
|
||||||
'react/react-in-jsx-scope': 'off'
|
'react/react-in-jsx-scope': 'off'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @nhost-examples/nextjs-server-components
|
# @nhost-examples/nextjs-server-components
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- e469628eb: fix: resolve issue with WebAuthn authentication
|
||||||
|
|
||||||
## 0.1.1
|
## 0.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/nextjs-server-components",
|
"name": "@nhost-examples/nextjs-server-components",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"eslint": "8.48.0",
|
"eslint": "8.48.0",
|
||||||
"eslint-config-next": "13.4.19",
|
"eslint-config-next": "13.4.19",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
|
"graphql": "16.7.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"postcss": "8.4.29",
|
"postcss": "8.4.29",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function SignInWithSecurityKey() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { sameSite: 'strict' })
|
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { path: '/' })
|
||||||
router.push('/protected/todos')
|
router.push('/protected/todos')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function SignUpWebAuthn() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { sameSite: 'strict' })
|
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { path: '/' })
|
||||||
router.push('/protected/todos')
|
router.push('/protected/todos')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getNhost } from '@utils/nhost'
|
|||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
const PAT = async ({
|
const PATs = async ({
|
||||||
params
|
params
|
||||||
}: {
|
}: {
|
||||||
params: {
|
params: {
|
||||||
@@ -100,4 +100,4 @@ const PAT = async ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthAsync(PAT)
|
export default withAuthAsync(PATs)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const TodoItem = ({ todo }: { todo: Todo }) => {
|
|||||||
<Link
|
<Link
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
passHref
|
||||||
href={nhost.storage.getPublicUrl({ fileId: todo.attachment.id })}
|
href={nhost.storage.getPublicUrl({ fileId: todo.attachment.id })}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { manageAuthSession } from '@utils/nhost'
|
import { manageAuthSession } from '@utils/nhost'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @next/next/no-server-import-in-page
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { AuthErrorPayload, NhostClient, NhostSession } from '@nhost/nhost-js'
|
import { AuthErrorPayload, NhostClient, NhostSession } from '@nhost/nhost-js'
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @next/next/no-server-import-in-page
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { type StateFrom } from 'xstate/lib/types'
|
import { type StateFrom } from 'xstate/lib/types'
|
||||||
import { waitFor } from 'xstate/lib/waitFor'
|
import { waitFor } from 'xstate/lib/waitFor'
|
||||||
|
|
||||||
export const NHOST_SESSION_KEY = 'nhost-session'
|
export const NHOST_SESSION_KEY = 'nhostSession'
|
||||||
|
|
||||||
export const getNhost = async (request?: NextRequest) => {
|
export const getNhost = async (request?: NextRequest) => {
|
||||||
const $cookies = request?.cookies || cookies()
|
const $cookies = request?.cookies || cookies()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -10,3 +10,4 @@
|
|||||||
- "!include public_todos.yaml"
|
- "!include public_todos.yaml"
|
||||||
- "!include storage_buckets.yaml"
|
- "!include storage_buckets.yaml"
|
||||||
- "!include storage_files.yaml"
|
- "!include storage_files.yaml"
|
||||||
|
- "!include storage_virus.yaml"
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ httpPoolSize = 100
|
|||||||
version = 18
|
version = 18
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
version = '0.21.3'
|
version = '0.24.0'
|
||||||
|
|
||||||
[auth.redirections]
|
[auth.redirections]
|
||||||
clientUrl = 'http://localhost:3000'
|
clientUrl = 'http://localhost:3000'
|
||||||
|
allowedUrls = ['https://example-nextjs-server-components.nhost.io', 'https://example-sveltekit.nhost.io']
|
||||||
|
|
||||||
[auth.signUp]
|
[auth.signUp]
|
||||||
enabled = true
|
enabled = true
|
||||||
@@ -129,8 +130,9 @@ enabled = false
|
|||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
[auth.method.webauthn.relyingParty]
|
[auth.method.webauthn.relyingParty]
|
||||||
name = 'nextjs-server-components'
|
name = 'quickstarts'
|
||||||
origins = ['http://localhost:3000']
|
id = 'examples.nhost.io'
|
||||||
|
origins = ['https://sveltekit.examples.nhost.io', 'https://nextjs-server-components.examples.nhost.io']
|
||||||
|
|
||||||
[auth.method.webauthn.attestation]
|
[auth.method.webauthn.attestation]
|
||||||
timeout = 60000
|
timeout = 60000
|
||||||
|
|||||||
15
examples/quickstarts/nhost-backend/nhost/overlays/local.json
Normal file
15
examples/quickstarts/nhost-backend/nhost/overlays/local.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "remove",
|
||||||
|
"path": "/auth/method/webauthn/relyingParty/id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "http://localhost:3000",
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/auth/method/webauthn/relyingParty/origins/0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "remove",
|
||||||
|
"path": "/auth/method/webauthn/relyingParty/origins/1"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.8.1",
|
|
||||||
"graphql": "^16.7.1",
|
"graphql": "^16.7.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* 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 */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry'
|
trace: 'on-first-retry'
|
||||||
@@ -51,7 +51,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'pnpm dev',
|
command: 'pnpm dev',
|
||||||
url: 'http://localhost:5173',
|
url: 'http://localhost:3000',
|
||||||
reuseExistingServer: !process.env.CI
|
reuseExistingServer: !process.env.CI
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
8
examples/quickstarts/sveltekit/pnpm-lock.yaml
generated
8
examples/quickstarts/sveltekit/pnpm-lock.yaml
generated
@@ -11,8 +11,8 @@ dependencies:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nhost/nhost-js':
|
'@nhost/nhost-js':
|
||||||
specifier: 2.2.17
|
specifier: 2.2.18
|
||||||
version: 2.2.17(graphql@16.8.0)
|
version: 2.2.18(graphql@16.8.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ packages:
|
|||||||
- encoding
|
- encoding
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@nhost/nhost-js@2.2.17(graphql@16.8.0):
|
/@nhost/nhost-js@2.2.18(graphql@16.8.0):
|
||||||
resolution: {integrity: sha512-6KRzhqmx7JcOmbp91/YZaBavGKdyGdx7kDrzRLoP1RYYOAIMpdMhHzeIju9LQfujY/8nFARRq97vFpPSbpnhSg==}
|
resolution: {integrity: sha512-aHn6p75fuG7SEUyB/yfX5TXtVTqwCT88zdN9Mmgo/8hnFOGV1XM7B4fxuGpNQCz18tG6kjM24tWx8EGXAEZ1sw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
|
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { redirect } from '@sveltejs/kit'
|
|||||||
|
|
||||||
/** @type {import('./$types').Actions} */
|
/** @type {import('./$types').Actions} */
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ cookies }) => {
|
default: async ({ request, cookies }) => {
|
||||||
const nhost = await getNhost(cookies)
|
const nhost = await getNhost(cookies)
|
||||||
|
const { providerUrl } = await nhost.auth.signIn({
|
||||||
const { providerUrl } = await nhost.auth.signIn({ provider: 'google' })
|
provider: 'google',
|
||||||
|
options: {
|
||||||
|
redirectTo: new URL(request.url).origin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (providerUrl) {
|
if (providerUrl) {
|
||||||
throw redirect(307, providerUrl)
|
throw redirect(307, providerUrl)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getNhost } from '$lib/nhost'
|
import { getNhost } from '$lib/nhost'
|
||||||
import { gql } from '@apollo/client'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
/** @type {import('./$types').PageServerLoad} */
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
export const load = async ({ url, cookies }) => {
|
export const load = async ({ url, cookies }) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getNhost } from '$lib/nhost.js'
|
import { getNhost } from '$lib/nhost.js'
|
||||||
import { gql } from '@apollo/client'
|
|
||||||
import { json, redirect } from '@sveltejs/kit'
|
import { json, redirect } from '@sveltejs/kit'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const DELETE = async ({ cookies, params }) => {
|
export const DELETE = async ({ cookies, params }) => {
|
||||||
const nhost = await getNhost(cookies)
|
const nhost = await getNhost(cookies)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getNhost } from '$lib/nhost'
|
import { getNhost } from '$lib/nhost'
|
||||||
import { gql } from '@apollo/client'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
/** @type {import('./$types').PageServerLoad} */
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
export const load = async ({ url, cookies }) => {
|
export const load = async ({ url, cookies }) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getNhost } from '$lib/nhost'
|
import { getNhost } from '$lib/nhost'
|
||||||
import { gql } from '@apollo/client'
|
|
||||||
import { redirect } from '@sveltejs/kit'
|
import { redirect } from '@sveltejs/kit'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
/** @type {import('./$types').Actions} */
|
/** @type {import('./$types').Actions} */
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getNhost } from '$lib/nhost'
|
import { getNhost } from '$lib/nhost'
|
||||||
import { gql } from '@apollo/client'
|
|
||||||
import { redirect } from '@sveltejs/kit'
|
import { redirect } from '@sveltejs/kit'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
/** @type {import('./$types').Actions} */
|
/** @type {import('./$types').Actions} */
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getNhost } from '$lib/nhost'
|
import { getNhost } from '$lib/nhost'
|
||||||
import { gql } from '@apollo/client'
|
|
||||||
import { json } from '@sveltejs/kit'
|
import { json } from '@sveltejs/kit'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
/** @type {import('./$types').RequestHandler} */
|
/** @type {import('./$types').RequestHandler} */
|
||||||
export async function POST({ request, cookies }) {
|
export async function POST({ request, cookies }) {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @nhost-examples/react-apollo
|
# @nhost-examples/react-apollo
|
||||||
|
|
||||||
|
## 0.1.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 67b2c044b: feat: add sign-in with Linked-In
|
||||||
|
|
||||||
## 0.1.16
|
## 0.1.16
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ httpPoolSize = 100
|
|||||||
version = 18
|
version = 18
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
version = '0.21.4'
|
version = '0.22.1'
|
||||||
|
|
||||||
[auth.redirections]
|
[auth.redirections]
|
||||||
clientUrl = 'https://react-apollo.example.nhost.io/'
|
clientUrl = 'https://react-apollo.example.nhost.io/'
|
||||||
@@ -112,7 +112,9 @@ clientId = '{{ secrets.GOOGLE_CLIENT_ID }}'
|
|||||||
clientSecret = '{{ secrets.GOOGLE_CLIENT_SECRET }}'
|
clientSecret = '{{ secrets.GOOGLE_CLIENT_SECRET }}'
|
||||||
|
|
||||||
[auth.method.oauth.linkedin]
|
[auth.method.oauth.linkedin]
|
||||||
enabled = false
|
enabled = true
|
||||||
|
clientId='{{ secrets.LINKEDIN_CLIENT_ID }}'
|
||||||
|
clientSecret='{{ secrets.LINKEDIN_CLIENT_SECRET }}'
|
||||||
|
|
||||||
[auth.method.oauth.spotify]
|
[auth.method.oauth.spotify]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/react-apollo",
|
"name": "@nhost-examples/react-apollo",
|
||||||
"version": "0.1.16",
|
"version": "0.1.17",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.7.14",
|
"@apollo/client": "^3.7.14",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { FaApple, 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 { useProviderLink } from '@nhost/react'
|
||||||
|
|
||||||
import AuthLink from './AuthLink'
|
import AuthLink from './AuthLink'
|
||||||
|
|
||||||
export default function OauthLinks() {
|
export default function OauthLinks() {
|
||||||
const { github, google, apple } = useProviderLink({ redirectTo: window.location.origin })
|
const { github, google, apple, linkedin } = useProviderLink({
|
||||||
|
redirectTo: window.location.origin
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -18,6 +20,10 @@ export default function OauthLinks() {
|
|||||||
<AuthLink leftIcon={<FaApple />} link={apple} color="#333333">
|
<AuthLink leftIcon={<FaApple />} link={apple} color="#333333">
|
||||||
Sign In With Apple
|
Sign In With Apple
|
||||||
</AuthLink>
|
</AuthLink>
|
||||||
|
|
||||||
|
<AuthLink leftIcon={<FaLinkedin />} link={linkedin} color="#0073B1">
|
||||||
|
Sign In With LinkedIn
|
||||||
|
</AuthLink>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
"build:docs": "turbo run build --filter=@nhost/docs",
|
"build:docs": "turbo run build --filter=@nhost/docs",
|
||||||
"build:all": "turbo run build --include-dependencies",
|
"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",
|
"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: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": "rm -rf ./{{packages,examples/**}/*,docs,dashboard}/{dist,umd,.next,.turbo,coverage}",
|
||||||
"ci:version": "changeset version && pnpm install --frozen-lockfile false",
|
"ci:version": "changeset version && pnpm install --frozen-lockfile false",
|
||||||
"coverage": "pnpm run test -- --coverage",
|
"coverage": "pnpm run test -- --coverage",
|
||||||
"prettier": "prettier --check .",
|
"prettier": "prettier --check .",
|
||||||
|
|||||||
1499
pnpm-lock.yaml
generated
1499
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -6,4 +6,5 @@ packages:
|
|||||||
- '!**/test/**'
|
- '!**/test/**'
|
||||||
- '!out/**'
|
- '!out/**'
|
||||||
- 'examples/*'
|
- 'examples/*'
|
||||||
|
- 'examples/quickstarts/*'
|
||||||
- '!**/functions'
|
- '!**/functions'
|
||||||
|
|||||||
Reference in New Issue
Block a user