mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
Merge branch 'master' into hi/improve-loading-time
This commit is contained in:
commit
2692310024
201 files changed
+7197
-4904
No files matched your search
Generated
+3
-3
@@ -13606,9 +13606,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
|
||||
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
image: supabase/gotrue:v2.5.8
|
||||
image: supabase/gotrue:v2.5.21
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
+3
-3
@@ -15068,9 +15068,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
|
||||
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
|
||||
+10
-14
@@ -6884,6 +6884,11 @@
|
||||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
|
||||
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||
@@ -6916,27 +6921,18 @@
|
||||
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
|
||||
},
|
||||
"postcss": {
|
||||
"version": "7.0.35",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
|
||||
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
|
||||
"version": "7.0.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
|
||||
"integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
|
||||
"requires": {
|
||||
"chalk": "^2.4.2",
|
||||
"source-map": "^0.6.1",
|
||||
"supports-color": "^6.1.0"
|
||||
"picocolors": "^0.2.1",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
|
||||
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+3
-3
@@ -21387,9 +21387,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
|
||||
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
|
||||
@@ -8546,9 +8546,9 @@ querystring@0.2.0:
|
||||
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
|
||||
|
||||
querystringify@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
|
||||
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
|
||||
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
|
||||
|
||||
raf@^3.4.1:
|
||||
version "3.4.1"
|
||||
@@ -10337,9 +10337,9 @@ url-loader@2.3.0:
|
||||
schema-utils "^2.5.0"
|
||||
|
||||
url-parse@^1.4.3:
|
||||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
|
||||
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
|
||||
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
|
||||
dependencies:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
+3
-3
@@ -17009,9 +17009,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.7.tgz",
|
||||
"integrity": "sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
|
||||
@@ -10982,9 +10982,9 @@ url-loader@4.1.1:
|
||||
schema-utils "^3.0.0"
|
||||
|
||||
url-parse@^1.4.3:
|
||||
version "1.5.7"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a"
|
||||
integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
|
||||
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
|
||||
dependencies:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
+6
-6
@@ -19486,9 +19486,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
|
||||
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
@@ -36791,9 +36791,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
|
||||
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
|
||||
+6
-6
@@ -14207,9 +14207,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
|
||||
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
@@ -27727,9 +27727,9 @@
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
|
||||
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
|
||||
@@ -72,29 +72,35 @@ const InviteUserModal = () => {
|
||||
size="small"
|
||||
key="invite-user-modal"
|
||||
visible={visible}
|
||||
title="Invite new user"
|
||||
header="Invite new user"
|
||||
hideFooter
|
||||
onCancel={handleToggle}
|
||||
closable
|
||||
>
|
||||
<Input
|
||||
label="User email"
|
||||
icon={<IconMail />}
|
||||
autoFocus
|
||||
// @ts-ignore
|
||||
ref={inputRef}
|
||||
value={emailValue}
|
||||
onChange={onInputChange}
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="User email"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Button onClick={onInviteUser} loading={loading} disabled={loading} block>
|
||||
Invite user
|
||||
</Button>
|
||||
<div className="py-4 space-y-6">
|
||||
<Modal.Content>
|
||||
<Input
|
||||
label="User email"
|
||||
icon={<IconMail />}
|
||||
descriptionText="Type in the full email address"
|
||||
autoFocus
|
||||
// @ts-ignore
|
||||
ref={inputRef}
|
||||
value={emailValue}
|
||||
onChange={onInputChange}
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="User email"
|
||||
className="w-full"
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Content>
|
||||
<Button onClick={onInviteUser} size="small" loading={loading} disabled={loading} block>
|
||||
Invite user
|
||||
</Button>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -115,6 +115,7 @@ const UserDropdown: FC<{ user: any }> = ({ user }) => {
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
size="medium"
|
||||
overlay={
|
||||
<>
|
||||
{user.email !== null ? (
|
||||
@@ -132,7 +133,7 @@ const UserDropdown: FC<{ user: any }> = ({ user }) => {
|
||||
Send OTP
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Divider light />
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item onClick={handleDelete} icon={<IconTrash size="tiny" />}>
|
||||
Delete user
|
||||
</Dropdown.Item>
|
||||
|
||||
@@ -45,10 +45,10 @@ const Users = () => {
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex justify-between px-6 pt-6 pb-2">
|
||||
<div className="relative flex space-x-1">
|
||||
<Input
|
||||
size="tiny"
|
||||
size="small"
|
||||
value={PageState.filterInputValue}
|
||||
onChange={onFilterChange}
|
||||
onKeyDown={onFilterKeyPress}
|
||||
@@ -68,12 +68,21 @@ const Users = () => {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Button className="mr-2" size="tiny" icon={<IconRefreshCw />} type="default" onClick={refreshUsers}>Reload</Button>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-2"
|
||||
size="tiny"
|
||||
icon={<IconRefreshCw />}
|
||||
type="default"
|
||||
loading={PageState.usersLoading}
|
||||
onClick={refreshUsers}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
{inviteEnabled && <InviteUserModal />}
|
||||
</div>
|
||||
</div>
|
||||
<section className="overflow-visible mt-4">
|
||||
<section className="overflow-visible mt-4 px-6">
|
||||
<div className="relative section-block--body rounded">
|
||||
<div className="align-middle inline-block min-w-full">
|
||||
<UsersList />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { FC } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Badge, Typography } from '@supabase/ui'
|
||||
import { Badge, IconUser, Typography } from '@supabase/ui'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
@@ -16,20 +16,22 @@ const UserListItem: FC<Props> = ({ user }) => {
|
||||
return (
|
||||
<Table.tr key={user.id}>
|
||||
<Table.td className="whitespace-nowrap">
|
||||
<Typography.Text>{!user.email ? '-' : user.email}</Typography.Text>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-scale-1200">{!user.email ? '-' : user.email}</span>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className="whitespace-nowrap">
|
||||
<Typography.Text>{!user.phone ? '-' : user.phone}</Typography.Text>
|
||||
<span className="text-scale-1200">{!user.phone ? '-' : user.phone}</span>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<Typography.Text type="secondary" className="capitalize">
|
||||
<span className="text-scale-1200 capitalize">
|
||||
{user?.raw_app_meta_data?.provider || user?.app_metadata?.provider}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<Typography.Text type="secondary" small>
|
||||
<span className="text-scale-1200">
|
||||
{dayjs(user.created_at).format('DD MMM, YYYY HH:mm')}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="hidden xl:table-cell">
|
||||
{!isUserConfirmed ? (
|
||||
|
||||
@@ -18,13 +18,13 @@ const UsersPagination = () => {
|
||||
return (
|
||||
<nav className="flex items-center justify-between overflow-hidden" aria-label="Pagination">
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-xs text-gray-400">
|
||||
<p className="text-xs text-scale-900">
|
||||
Showing
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.fromRow}</span>
|
||||
<span className="px-1 font-medium text-scale-1100">{PageState.fromRow}</span>
|
||||
to
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.toRow}</span>
|
||||
<span className="px-1 font-medium text-scale-1100">{PageState.toRow}</span>
|
||||
of
|
||||
<span className="px-1 font-medium text-gray-400">{PageState.totalUsers}</span>
|
||||
<span className="px-1 font-medium text-scale-1100">{PageState.totalUsers}</span>
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ const UpgradeButton: FC<Props> = ({ projectRef, paid, subscriptionStats }) => {
|
||||
|
||||
function handleUpgradeButton() {
|
||||
if (paid) {
|
||||
if (freeProjectsOwned >= freeProjectsLimit) {
|
||||
if (freeProjectsOwned >= DEFAULT_FREE_PROJECTS_LIMIT) {
|
||||
setShowFreeProjectLimitWarning(true)
|
||||
} else {
|
||||
setExitSurveyVisible(true)
|
||||
@@ -76,7 +76,7 @@ const UpgradeButton: FC<Props> = ({ projectRef, paid, subscriptionStats }) => {
|
||||
closable
|
||||
visible={showFreeProjectLimitWarning}
|
||||
onCancel={() => setShowFreeProjectLimitWarning(false)}
|
||||
title="Free project limit reached"
|
||||
header="Free project limit reached"
|
||||
description="Please delete one of your free projects to downgrade this project back to free tier"
|
||||
size="tiny"
|
||||
showIcon
|
||||
@@ -92,7 +92,13 @@ const UpgradeButton: FC<Props> = ({ projectRef, paid, subscriptionStats }) => {
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="text-sm text-scale-1100 py-4">
|
||||
Please delete one of your free projects to downgrade this project back to free tier
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
<div className="flex flex-col items-end space-y-2">
|
||||
<Button
|
||||
disabled={!isOrgOwner}
|
||||
@@ -158,6 +164,7 @@ function UnsubcribeExitSurvey({ visible, setVisible, handleDowngrade }: any) {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header="Reason for cancelling"
|
||||
loading={loading}
|
||||
visible={visible}
|
||||
closable
|
||||
@@ -165,23 +172,24 @@ function UnsubcribeExitSurvey({ visible, setVisible, handleDowngrade }: any) {
|
||||
hideFooter
|
||||
onCancel={() => setVisible(false)}
|
||||
>
|
||||
<div>
|
||||
<div className="px-6 pb-4">
|
||||
<Typography.Title level={3} className="m-0">
|
||||
Reason for cancelling
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<Divider light />
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
<Typography.Text type="secondary" className="mb-2">
|
||||
<p>We always strive to improve Supabase as much as we can.</p>
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<p>
|
||||
please let us know the reasons you are cancelling your subscription so we can improve
|
||||
in the future.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<div className="py-4 space-y-4">
|
||||
<Modal.Content>
|
||||
<div className="">
|
||||
<div>
|
||||
<p className="text-base text-scale-1200">
|
||||
<p>We always strive to improve Supabase as much as we can.</p>
|
||||
</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
<p>
|
||||
please let us know the reasons you are cancelling your subscription so we can
|
||||
improve in the future.
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Seperator />
|
||||
<Modal.Content>
|
||||
<div className="flex flex-wrap gap-2" data-toggle="buttons">
|
||||
{CancellationReasons.map((option) => {
|
||||
const active = selectedCancellationReasons.find((x) => x === option)
|
||||
@@ -194,8 +202,8 @@ function UnsubcribeExitSurvey({ visible, setVisible, handleDowngrade }: any) {
|
||||
duration-100 cursor-pointer text-sm
|
||||
${
|
||||
active
|
||||
? ` opacity-100 bg-white text-typography-body-light hover:bg-opacity-75`
|
||||
: ` bg-gray-500 opacity-25 hover:opacity-50 text-typography-body-dark`
|
||||
? ` opacity-100 bg-white text-scale-100 hover:bg-opacity-75`
|
||||
: ` bg-scale-1200 opacity-25 hover:opacity-50 text-scale-100`
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -211,9 +219,9 @@ function UnsubcribeExitSurvey({ visible, setVisible, handleDowngrade }: any) {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Divider light />
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
</Modal.Content>
|
||||
<Modal.Seperator />
|
||||
<Modal.Content>
|
||||
<Input.TextArea
|
||||
id="feedback"
|
||||
onChange={(e) => setAdditionalFeedback(e.target.value)}
|
||||
@@ -221,9 +229,9 @@ function UnsubcribeExitSurvey({ visible, setVisible, handleDowngrade }: any) {
|
||||
label="Anything else we can improve on?"
|
||||
placeholder="Anything else we should know, or could improve on?"
|
||||
/>
|
||||
</div>
|
||||
<Divider light />
|
||||
<div className="flex justify-end px-6 py-6 w-full space-x-2">
|
||||
</Modal.Content>
|
||||
<Modal.Seperator />
|
||||
<div className="flex gap-2 justify-end px-6 w-full">
|
||||
<Button type="default" onClick={() => setVisible(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -42,58 +42,50 @@ const BackupsList: FC<Props> = ({}) => {
|
||||
const { backups, tierId } = projectData
|
||||
const sortedBackups = backups.sort((a: any, b: any) => b.id - a.id)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* <div className="mb-4">
|
||||
<BackupButton backups={sortedBackups} projectRef={projectRef} />
|
||||
</div> */}
|
||||
|
||||
{tierId === STRIPE_PRODUCT_IDS.FREE && (
|
||||
<div
|
||||
className={[
|
||||
'block w-full p-3 px-6 border rounded border-opacity-20',
|
||||
'bg-gray-100 border-gray-600',
|
||||
'dark:bg-gray-600 dark:border-gray-500',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconClock size="large" />
|
||||
</div>
|
||||
<div className="flex justify-between w-full items-center">
|
||||
if (tierId === STRIPE_PRODUCT_IDS.FREE) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'block w-full py-4 px-6 border rounded border-opacity-20',
|
||||
'bg-gray-100 border-gray-600',
|
||||
'dark:bg-gray-400 dark:border-gray-300',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconClock size="large" />
|
||||
</div>
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div>
|
||||
<p>Free Plan does not include project backups.</p>
|
||||
<div>
|
||||
<Typography.Text>Free Plan does not include project backups.</Typography.Text>
|
||||
<div>
|
||||
<Typography.Text type="secondary">
|
||||
Please upgrade to Pro plan for up to 7 days of backups.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Please upgrade to Pro plan for up to 7 days of backups.
|
||||
</p>
|
||||
</div>
|
||||
<Link href={`/project/${projectRef}/settings/billing`}>
|
||||
<Button type="primary" className="flex-grow">
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href={`/project/${projectRef}/settings/billing`}>
|
||||
<Button type="primary">Upgrade to Pro</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{!sortedBackups.length && tierId !== STRIPE_PRODUCT_IDS.FREE ? (
|
||||
<div className="block w-full bg-green-500 bg-opacity-5 p-3 border border-green-500 border-opacity-50 rounded">
|
||||
<div className="flex space-x-3">
|
||||
<div>
|
||||
<IconInfo className="text-green-500" size="large" />
|
||||
</div>
|
||||
<Typography.Text type="success">
|
||||
No backups created yet. Check again tomorrow.
|
||||
</Typography.Text>
|
||||
<p>No backups created yet. Check again tomorrow.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Panel
|
||||
// title={[<Typography.Title level={5}>Date</Typography.Title>]}
|
||||
>
|
||||
<Panel>
|
||||
{!sortedBackups && (
|
||||
<div className="text-center p-4">
|
||||
<img className="loading-spinner" src="/img/spinner.gif"></img>
|
||||
|
||||
@@ -112,17 +112,17 @@ const ExtensionCard: FC<Props> = ({ extension }) => {
|
||||
flex flex-col h-full"
|
||||
>
|
||||
<div className="p-4 px-6">
|
||||
<Typography.Text type="secondary">
|
||||
<span className="flex-grow capitalize-first">{extension.comment}</span>
|
||||
</Typography.Text>
|
||||
<p className="text-sm text-scale-1100">
|
||||
<span className="flex-grow capitalize">{extension.comment}</span>
|
||||
</p>
|
||||
</div>
|
||||
{isOn && extension.schema && (
|
||||
<div className="p-4 px-6">
|
||||
<Typography.Text type="secondary" small>
|
||||
<span className="flex-grow capitalize-first">
|
||||
<p className="text-sm text-scale-1100">
|
||||
<span className="flex-grow">
|
||||
Schema: <Badge>{`${extension.schema}`}</Badge>
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ const Extensions: FC<Props> = ({}) => {
|
||||
<div className="mb-4">
|
||||
<div className="flex">
|
||||
<Input
|
||||
size="tiny"
|
||||
size="small"
|
||||
placeholder={'Filter'}
|
||||
value={filterString}
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
@@ -38,7 +38,7 @@ const Extensions: FC<Props> = ({}) => {
|
||||
<div className="w-full my-8 space-y-12">
|
||||
{enabledExtensions.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<Typography.Title level={4}>Enabled</Typography.Title>
|
||||
<h4 className="text-lg">Enabled</h4>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3 mb-4">
|
||||
{enabledExtensions.map((extension) => (
|
||||
<ExtensionCard key={extension.name} extension={extension} />
|
||||
@@ -49,7 +49,7 @@ const Extensions: FC<Props> = ({}) => {
|
||||
|
||||
{disabledExtensions.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<Typography.Title level={4}>Extensions</Typography.Title>
|
||||
<h4 className="text-lg">Extensions</h4>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3 mb-4">
|
||||
{disabledExtensions.map((extension) => (
|
||||
<ExtensionCard key={extension.name} extension={extension} />
|
||||
|
||||
@@ -152,7 +152,7 @@ class CreateFunctionStore implements ICreateFunctionStore {
|
||||
get title() {
|
||||
return this.formState.id
|
||||
? `Edit '${this.formState.originalName}' function`
|
||||
: 'Add a new Function'
|
||||
: 'Add a new function'
|
||||
}
|
||||
|
||||
get isEditing() {
|
||||
@@ -350,13 +350,10 @@ const CreateFunction: FC<CreateFunctionProps> = ({ func, visible, setVisible })
|
||||
return (
|
||||
<>
|
||||
<SidePanel
|
||||
wide
|
||||
size="large"
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(!visible)}
|
||||
// @ts-ignore
|
||||
contentStyle={{ padding: 0 }}
|
||||
footerBackground={true}
|
||||
title={_localState.title}
|
||||
header={_localState.title}
|
||||
className="hooks-sidepanel transform transition-all duration-300 ease-in-out mr-0"
|
||||
loading={_localState.loading}
|
||||
onConfirm={handleSubmit}
|
||||
@@ -365,50 +362,73 @@ const CreateFunction: FC<CreateFunctionProps> = ({ func, visible, setVisible })
|
||||
<div className="space-y-10 mt-4">
|
||||
{_localState.isEditing ? (
|
||||
<>
|
||||
<div className="px-6 space-y-6">
|
||||
<InputName />
|
||||
<SelectSchema />
|
||||
</div>
|
||||
<Divider light />
|
||||
<div className="px-6 space-y-6">
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-4">
|
||||
<InputName />
|
||||
<SelectSchema />
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<InputMultiArguments readonly={true} />
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<InputDefinition />
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<InputName />
|
||||
<Divider light />
|
||||
<SelectSchema />
|
||||
<SelectReturnType />
|
||||
<Divider light />
|
||||
<InputMultiArguments />
|
||||
<Divider light />
|
||||
<InputDefinition />
|
||||
<Divider light />
|
||||
<div className="px-6">
|
||||
<SidePanel.Content>
|
||||
<InputName />
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-4">
|
||||
<SelectSchema />
|
||||
<SelectReturnType />
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<InputMultiArguments />
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<InputDefinition />
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<Panel>
|
||||
<div className={`space-y-8 py-4 dark:bg-bg-alt-dark rounded`}>
|
||||
<div className={`space-y-8 py-4 bg-bg-alt-light dark:bg-bg-alt-dark rounded`}>
|
||||
<div className={`px-6`}>
|
||||
<Toggle
|
||||
onChange={() => _localState.toggleAdvancedVisible()}
|
||||
label="Show advanced settings"
|
||||
checked={_localState.advancedVisible}
|
||||
labelOptional="These are settings that might be familiar for postgres heavy users "
|
||||
/>
|
||||
</div>
|
||||
{/* advanced selections */}
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
{_localState.advancedVisible && (
|
||||
<>
|
||||
{/* <Divider light /> */}
|
||||
<SelectLanguage />
|
||||
<SelectBehavior />
|
||||
<Divider light />
|
||||
<InputMultiConfigParams />
|
||||
<Divider light />
|
||||
<RadioSecurity />
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-2">
|
||||
<SelectLanguage />
|
||||
<SelectBehavior />
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<InputMultiConfigParams />
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<RadioSecurity />
|
||||
</SidePanel.Content>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -425,24 +445,22 @@ export default observer(CreateFunction)
|
||||
const InputName: FC = observer(({}) => {
|
||||
const _localState = useContext(CreateFunctionContext)
|
||||
return (
|
||||
<div className={'px-6'}>
|
||||
<Input
|
||||
id="name"
|
||||
label="Name of function"
|
||||
layout="horizontal"
|
||||
placeholder="Name of function"
|
||||
value={_localState!.formState.name.value}
|
||||
onChange={(e) =>
|
||||
_localState!.onFormChange({
|
||||
key: 'name',
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
size="small"
|
||||
error={_localState!.formState.name.error}
|
||||
descriptionText="Name will also be used for the function name in postgres"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="name"
|
||||
label="Name of function"
|
||||
layout="horizontal"
|
||||
placeholder="Name of function"
|
||||
value={_localState!.formState.name.value}
|
||||
onChange={(e) =>
|
||||
_localState!.onFormChange({
|
||||
key: 'name',
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
size="small"
|
||||
error={_localState!.formState.name.error}
|
||||
descriptionText="Name will also be used for the function name in postgres"
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -462,18 +480,16 @@ const InputMultiArguments: FC<InputMultiArgumentsProps> = observer(({ readonly }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'px-6'}>
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<Typography.Text>Arguments</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<h5 className="text-base text-scale-1200">Arguments</h5>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Arguments can be referenced in the function body using either names or numbers.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-4 space-y-2">
|
||||
{readonly && isEmpty(_localState!.formState.args.value) && (
|
||||
<Typography.Text type="secondary" className="opacity-50">
|
||||
No argument for this function
|
||||
</Typography.Text>
|
||||
<span className="text-scale-900">No argument for this function</span>
|
||||
)}
|
||||
{_localState!.formState.args.value.map(
|
||||
(x: { name: string; type: string; error?: string }, idx: number) => (
|
||||
@@ -489,7 +505,7 @@ const InputMultiArguments: FC<InputMultiArgumentsProps> = observer(({ readonly }
|
||||
)}
|
||||
{!readonly && (
|
||||
<div className="">
|
||||
<Button type="dashed" icon={<IconPlus />} onClick={onAddArgument} disabled={readonly}>
|
||||
<Button type="default" icon={<IconPlus />} onClick={onAddArgument} disabled={readonly}>
|
||||
Add a new argument
|
||||
</Button>
|
||||
</div>
|
||||
@@ -538,7 +554,7 @@ const InputArgument: FC<InputArgumentProps> = observer(({ idx, name, type, error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-row space-x-1`}>
|
||||
<div className="flex flex-row space-x-1">
|
||||
<Input
|
||||
id={`name-${idx}`}
|
||||
className="flex-1 flex-grow"
|
||||
@@ -568,7 +584,7 @@ const InputArgument: FC<InputArgumentProps> = observer(({ idx, name, type, error
|
||||
<div>
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
type="default"
|
||||
icon={<IconTrash size="tiny" />}
|
||||
onClick={onDelete}
|
||||
size="small"
|
||||
@@ -591,9 +607,9 @@ const InputMultiConfigParams: FC = observer(({}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'px-6'}>
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<Typography.Text>Config Params</Typography.Text>
|
||||
<h5 className="text-base text-scale-1200">Config Params</h5>
|
||||
</div>
|
||||
<div className="pt-4 space-y-2">
|
||||
{_localState!.formState.configParams.value.map(
|
||||
@@ -612,7 +628,7 @@ const InputMultiConfigParams: FC = observer(({}) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<Button type="dashed" icon={<IconPlus />} onClick={onAddArgument}>
|
||||
<Button type="default" icon={<IconPlus />} onClick={onAddArgument}>
|
||||
Add a new config
|
||||
</Button>
|
||||
</div>
|
||||
@@ -658,7 +674,7 @@ const InputConfigParam: FC<InputConfigParamProps> = observer(({ idx, name, value
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex space-x-1`}>
|
||||
<div className="flex space-x-1">
|
||||
<Input
|
||||
id={`name-${idx}`}
|
||||
className="flex-1"
|
||||
@@ -680,7 +696,7 @@ const InputConfigParam: FC<InputConfigParamProps> = observer(({ idx, name, value
|
||||
<div>
|
||||
<Button
|
||||
danger
|
||||
type="primary"
|
||||
type="default"
|
||||
icon={<IconTrash size="tiny" />}
|
||||
onClick={onDelete}
|
||||
size="small"
|
||||
@@ -693,15 +709,15 @@ const InputConfigParam: FC<InputConfigParamProps> = observer(({ idx, name, value
|
||||
const InputDefinition: FC = observer(({}) => {
|
||||
const _localState = useContext(CreateFunctionContext)
|
||||
return (
|
||||
<div className={`space-y-4 px-6`}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col">
|
||||
<Typography.Text>Definition</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<h5 className="text-base text-scale-1200">Definition</h5>
|
||||
<p className="text-sm text-scale-1100">
|
||||
The language below should be written in `{_localState!.formState.language.value}`.
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Change the language in the Advanced Settings below.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-40 border dark:border-dark">
|
||||
{/* @ts-ignore */}
|
||||
@@ -725,29 +741,27 @@ const SelectSchema: FC = observer(({}) => {
|
||||
const _localState = useContext(CreateFunctionContext)
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 px-6`}>
|
||||
<Select
|
||||
id="schema"
|
||||
label="Schema"
|
||||
layout="horizontal"
|
||||
value={_localState!.formState.schema.value}
|
||||
onChange={(e) =>
|
||||
_localState!.onFormChange({
|
||||
key: 'schema',
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Pick a schema"
|
||||
size="small"
|
||||
descriptionText="Tables made in the table editor will be in 'public'"
|
||||
>
|
||||
{_localState!.schemas.map((x) => (
|
||||
<Select.Option key={x.name} value={x.name}>
|
||||
{x.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
id="schema"
|
||||
label="Schema"
|
||||
layout="horizontal"
|
||||
value={_localState!.formState.schema.value}
|
||||
onChange={(e) =>
|
||||
_localState!.onFormChange({
|
||||
key: 'schema',
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Pick a schema"
|
||||
size="small"
|
||||
descriptionText="Tables made in the table editor will be in 'public'"
|
||||
>
|
||||
{_localState!.schemas.map((x) => (
|
||||
<Select.Option key={x.name} value={x.name}>
|
||||
{x.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -761,7 +775,7 @@ const SelectLanguage: FC = observer(({}) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 px-6`}>
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
id="language"
|
||||
label="Language"
|
||||
@@ -798,7 +812,7 @@ const SelectReturnType: FC = observer(({}) => {
|
||||
const _localState = useContext(CreateFunctionContext)
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 px-6`}>
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
id="returnType"
|
||||
label="Return type"
|
||||
@@ -830,7 +844,7 @@ const SelectBehavior: FC = observer(({}) => {
|
||||
const _localState = useContext(CreateFunctionContext)
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 px-6`}>
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
id="behavior"
|
||||
label="Behavior"
|
||||
@@ -857,7 +871,7 @@ const RadioSecurity: FC = observer(({}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`space-y-4 px-6`}>
|
||||
<div className="space-y-4">
|
||||
<Radio.Group
|
||||
type="cards"
|
||||
label="Type of security"
|
||||
@@ -878,7 +892,7 @@ const RadioSecurity: FC = observer(({}) => {
|
||||
label="SECURITY INVOKER"
|
||||
value="SECURITY_INVOKER"
|
||||
checked={!_localState!.formState.securityDefiner.value}
|
||||
description="function is to be executed with the privileges of the user that calls it."
|
||||
description="Function is to be executed with the privileges of the user that calls it."
|
||||
/>
|
||||
<Radio
|
||||
id="SECURITY_DEFINER"
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Dictionary } from '@supabase/grid'
|
||||
import {
|
||||
Input,
|
||||
SidePanel,
|
||||
Divider,
|
||||
Checkbox,
|
||||
Listbox,
|
||||
Typography,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
Badge,
|
||||
IconTrash,
|
||||
IconPlus,
|
||||
Alert,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import Image from 'next/image'
|
||||
@@ -178,8 +178,8 @@ class CreateHookStore implements ICreateHookStore {
|
||||
|
||||
get title() {
|
||||
return this.formState.id
|
||||
? `Edit '${this.formState.originalName}' Function Hook`
|
||||
: 'Add a new Function Hook'
|
||||
? `Edit '${this.formState.originalName}' function hook`
|
||||
: 'Add a new function hook'
|
||||
}
|
||||
|
||||
get isEditing() {
|
||||
@@ -428,13 +428,10 @@ const CreateHook: FC<CreateHookProps> = ({ hook, visible = true, setVisible }) =
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
size="large"
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(!visible)}
|
||||
// @ts-ignore
|
||||
contentStyle={{ padding: 0 }}
|
||||
footerBackground={true}
|
||||
title={_localState.title}
|
||||
header={_localState.title}
|
||||
className={
|
||||
'hooks-sidepanel transform transition-all duration-300 ease-in-out mr-0 ' + '' // (showCreateFunctionSidePanel ? 'mr-16' : '')
|
||||
}
|
||||
@@ -442,9 +439,9 @@ const CreateHook: FC<CreateHookProps> = ({ hook, visible = true, setVisible }) =
|
||||
onConfirm={handleSubmit}
|
||||
>
|
||||
<CreateHookContext.Provider value={_localState}>
|
||||
<div className="space-y-6 mt-4 pb-8">
|
||||
<div className="space-y-10 py-6">
|
||||
{_localState.isEditing ? (
|
||||
<div className="px-6 space-y-6">
|
||||
<div className="px-6 space-y-10 py-6">
|
||||
<InputName />
|
||||
<SelectEnabledMode />
|
||||
</div>
|
||||
@@ -453,15 +450,15 @@ const CreateHook: FC<CreateHookProps> = ({ hook, visible = true, setVisible }) =
|
||||
<div className="px-6">
|
||||
<InputName />
|
||||
</div>
|
||||
<Divider light />
|
||||
<SidePanel.Seperator />
|
||||
<div className="px-6 space-y-6">
|
||||
<Typography.Title level={5}>Conditions to fire hook</Typography.Title>
|
||||
<h5 className="text-base text-scale-1200">Conditions to fire hook</h5>
|
||||
<ListboxTable />
|
||||
<CheckboxEvents />
|
||||
</div>
|
||||
<Divider light />
|
||||
<SidePanel.Seperator />
|
||||
<div className="px-6 space-y-6">
|
||||
<Typography.Title level={5}>Type of hook</Typography.Title>
|
||||
<h5 className="text-base text-scale-1200">Type of hook</h5>
|
||||
<RadioGroupHookService />
|
||||
</div>
|
||||
<ServiceConfigForm />
|
||||
@@ -546,18 +543,21 @@ const CheckboxEvents: FC = observer(({}) => {
|
||||
>
|
||||
<Checkbox
|
||||
value="INSERT"
|
||||
id="INSERT"
|
||||
label="Insert"
|
||||
description={'Any insert operation on the table'}
|
||||
checked={_localState.formState.events.value.includes('INSERT')}
|
||||
/>
|
||||
<Checkbox
|
||||
value="UPDATE"
|
||||
id="UPDATE"
|
||||
label="Update"
|
||||
description="Any update operation, of any column in the table"
|
||||
checked={_localState.formState.events.value.includes('UPDATE')}
|
||||
/>
|
||||
<Checkbox
|
||||
value="DELETE"
|
||||
id="DELETE"
|
||||
label="Delete"
|
||||
description="Any deletion of a record"
|
||||
checked={_localState.formState.events.value.includes('DELETE')}
|
||||
@@ -632,7 +632,7 @@ const ListboxTable: FC = observer(({}) => {
|
||||
value={x.id}
|
||||
label={x.name}
|
||||
addOnBefore={() => (
|
||||
<div className="bg-green-600 p-1 flex items-center justify-center rounded text-typography-body-dark ">
|
||||
<div className="bg-scale-1200 p-1 flex items-center justify-center rounded text-scale-100 ">
|
||||
<SVG
|
||||
src={'/img/table-editor.svg'}
|
||||
style={{ width: `16px`, height: `16px`, strokeWidth: '1px' }}
|
||||
@@ -645,7 +645,6 @@ const ListboxTable: FC = observer(({}) => {
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<Typography.Text>{x.name}</Typography.Text>
|
||||
<Divider light type="vertical" />
|
||||
<Typography.Text type="secondary" className="opacity-50" small>
|
||||
{x.schema}
|
||||
</Typography.Text>
|
||||
@@ -661,7 +660,7 @@ const hookServiceOptions: {
|
||||
id: string
|
||||
label: string
|
||||
badge: string
|
||||
badgeType: 'green' | 'blue'
|
||||
badgeType: 'brand' | 'amber'
|
||||
description: string
|
||||
img_url: string
|
||||
}[] = [
|
||||
@@ -669,7 +668,7 @@ const hookServiceOptions: {
|
||||
id: 'http_request',
|
||||
label: 'HTTP Request',
|
||||
badge: 'Alpha',
|
||||
badgeType: 'green',
|
||||
badgeType: 'brand',
|
||||
description: 'Send an HTTP request to any URL.',
|
||||
img_url: 'http-request.png',
|
||||
},
|
||||
@@ -677,7 +676,7 @@ const hookServiceOptions: {
|
||||
id: 'supabase_function',
|
||||
label: 'Supabase Function',
|
||||
badge: 'Coming soon',
|
||||
badgeType: 'blue',
|
||||
badgeType: 'amber',
|
||||
description: 'Choose a Supabase Function to run.',
|
||||
img_url: 'supabase-severless-function.png',
|
||||
},
|
||||
@@ -685,7 +684,7 @@ const hookServiceOptions: {
|
||||
id: 'google_cloud_function',
|
||||
label: 'Google cloud run',
|
||||
badge: 'Coming soon',
|
||||
badgeType: 'blue',
|
||||
badgeType: 'amber',
|
||||
description: 'Choose a google cloud function to run',
|
||||
img_url: 'google-cloud-run.png',
|
||||
},
|
||||
@@ -693,7 +692,7 @@ const hookServiceOptions: {
|
||||
id: 'aws_lambda_function',
|
||||
label: 'AWS Lambda',
|
||||
badge: 'Coming soon',
|
||||
badgeType: 'blue',
|
||||
badgeType: 'amber',
|
||||
description: 'Choose an AWS Lambda function to run.',
|
||||
img_url: 'aws-lambda-severless-function.png',
|
||||
},
|
||||
@@ -729,7 +728,7 @@ interface RadioHookService {
|
||||
description: string
|
||||
label: string
|
||||
img_url: string
|
||||
badgeType: 'green' | 'blue'
|
||||
badgeType: 'brand' | 'amber'
|
||||
badge: string
|
||||
}
|
||||
|
||||
@@ -739,13 +738,12 @@ const RadioHookService: FC<RadioHookService> = observer(
|
||||
return (
|
||||
<Radio
|
||||
id={id}
|
||||
// label={label}
|
||||
value={id}
|
||||
checked={_localState.formState.hookService.value == id}
|
||||
// @ts-ignore
|
||||
beforeLabel={
|
||||
<>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-5">
|
||||
{/* <div className="h-3 w-3"> */}
|
||||
<Image
|
||||
src={`/img/function-providers/${img_url}`}
|
||||
@@ -756,16 +754,14 @@ const RadioHookService: FC<RadioHookService> = observer(
|
||||
{/* </div> */}
|
||||
<div className="flex-col space-y-0">
|
||||
<div className="flex space-x-1">
|
||||
<Typography.Text>{label}</Typography.Text>
|
||||
<span className="text-scale-1200">{label}</span>
|
||||
<Badge color={badgeType}>{badge}</Badge>
|
||||
</div>
|
||||
<Typography.Text type="secondary">{description}</Typography.Text>
|
||||
<span className="text-scale-900">{description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
// @ts-ignore
|
||||
// description={description}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -777,7 +773,7 @@ const ServiceConfigForm: FC = observer(({}) => {
|
||||
<>
|
||||
{_localState.formState.hookService.value === 'http_request' ? (
|
||||
<>
|
||||
<Divider light />
|
||||
<SidePanel.Seperator />
|
||||
<div className="space-y-10">
|
||||
<div className="px-6 space-y-6">
|
||||
<Typography.Title level={5}>HTTP Request</Typography.Title>
|
||||
@@ -785,18 +781,20 @@ const ServiceConfigForm: FC = observer(({}) => {
|
||||
<InputServiceUrl />
|
||||
{/* <InputServiceTimeout /> */}
|
||||
</div>
|
||||
<Divider light />
|
||||
<SidePanel.Seperator />
|
||||
<div className="px-6">
|
||||
<InputMultiServiceHeaders />
|
||||
</div>
|
||||
<Divider light />
|
||||
<SidePanel.Seperator />
|
||||
<div className="px-6">
|
||||
<InputMultiServiceParams />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<ServiceUnavailableBox />
|
||||
<div className="px-6">
|
||||
<ServiceUnavailableBox />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -883,7 +881,7 @@ const InputMultiServiceHeaders: FC = observer(({}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<Typography.Text>Http headers</Typography.Text>
|
||||
<Typography.Text>HTTP Headers</Typography.Text>
|
||||
</div>
|
||||
<div className="pt-4 space-y-2">
|
||||
{_localState.formState.serviceHeaders.value.map(
|
||||
@@ -994,7 +992,7 @@ const InputMultiServiceParams: FC = observer(({}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<Typography.Text>Http params</Typography.Text>
|
||||
<Typography.Text>HTTP Params</Typography.Text>
|
||||
</div>
|
||||
<div className="pt-4 space-y-2">
|
||||
{_localState.formState.serviceParams.value.map(
|
||||
@@ -1092,37 +1090,22 @@ const InputServiceParam: FC<InputServiceParamsProps> = observer(({ idx, name, va
|
||||
const ServiceUnavailableBox: FC = observer(({}) => {
|
||||
const _localState: any = useContext(CreateHookContext)
|
||||
return (
|
||||
<div className="px-6">
|
||||
<div className="block w-full bg-yellow-500 bg-opacity-5 p-3 px-6 border border-yellow-600 border-opacity-40 rounded">
|
||||
<div className="flex space-x-3">
|
||||
{/* <div className="mt-1">
|
||||
<IconTool className="text-white" size="large" />
|
||||
</div> */}
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div>
|
||||
<Typography.Text>Service under development.</Typography.Text>
|
||||
<div>
|
||||
<Typography.Text type="secondary">
|
||||
We currently do not support this service.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="outline"
|
||||
className="flex-grow"
|
||||
onClick={() =>
|
||||
_localState.onFormChange({
|
||||
key: 'hookService',
|
||||
value: 'http_request',
|
||||
})
|
||||
}
|
||||
>
|
||||
Switch to HTTP
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Alert variant="warning" title="Service under development" withIcon>
|
||||
<div className="space-y-4">
|
||||
<div>We currently do not support this service.</div>
|
||||
<Button
|
||||
type="default"
|
||||
className="flex-grow"
|
||||
onClick={() =>
|
||||
_localState.onFormChange({
|
||||
key: 'hookService',
|
||||
value: 'http_request',
|
||||
})
|
||||
}
|
||||
>
|
||||
Switch to HTTP
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Input, Divider, Typography } from '@supabase/ui'
|
||||
import { Input, Typography } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { pluckObjectFields } from 'lib/helpers'
|
||||
@@ -9,6 +9,8 @@ import Panel from 'components/to-be-cleaned/Panel'
|
||||
import PgBouncerConfig from './PgBouncerConfig'
|
||||
import DownloadCertificate from './DownloadCertificate'
|
||||
|
||||
import Divider from 'components/ui/Divider'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const BouncerSettings: FC<Props> = ({}) => {
|
||||
@@ -163,7 +165,7 @@ const BouncerSettings: FC<Props> = ({}) => {
|
||||
/>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<Typography.Text type="secondary">
|
||||
<p className="text-sm text-scale-1100">
|
||||
<p>
|
||||
Learn more about connection strings{' '}
|
||||
<Typography.Link href="https://supabase.com/docs/reference/postgres/connection-strings">
|
||||
@@ -175,7 +177,7 @@ const BouncerSettings: FC<Props> = ({}) => {
|
||||
</Typography.Link>
|
||||
.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { Button, Typography, IconDownload } from '@supabase/ui'
|
||||
import { Button, IconDownload } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
createdAt: string
|
||||
@@ -9,14 +9,15 @@ const DownloadCertificate: FC<Props> = ({ createdAt }) => {
|
||||
// instances before 3 : 08 pm sgt 29th April don't have certs installed
|
||||
if (new Date(createdAt) < new Date('2021-04-30')) return null
|
||||
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div>
|
||||
<Typography.Text className="block">SSL Connection</Typography.Text>
|
||||
<p className="block">SSL Connection</p>
|
||||
<div>
|
||||
<Typography.Text type="secondary" className="opacity-50">
|
||||
<p className="text-sm text-scale-1100">
|
||||
Use this certificate to prevent snooping and man-in-the-middle attacks.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="default" icon={<IconDownload />}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Divider, Typography } from '@supabase/ui'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import Divider from 'components/ui/Divider'
|
||||
import { AutoField } from 'uniforms-bootstrap4'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
@@ -102,17 +103,18 @@ const PgbouncerConfig: FC<Props> = ({ projectRef, config }) => {
|
||||
errorMessage="You must select one of the three options"
|
||||
/>
|
||||
<div className="flex !mt-1" style={{ marginLeft: 'calc(33% + 0.5rem)' }}>
|
||||
<Typography.Text type="secondary">
|
||||
<p className="text-sm text-scale-900">
|
||||
Specify when a connection can be returned to the pool. To find out the most
|
||||
suitable mode for your use case,{' '}
|
||||
<Typography.Link
|
||||
<a
|
||||
className="text-green-900"
|
||||
target="_blank"
|
||||
href="https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool"
|
||||
>
|
||||
click here
|
||||
</Typography.Link>
|
||||
</a>
|
||||
.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<Divider light />
|
||||
{/* <AutoField
|
||||
|
||||
@@ -52,7 +52,7 @@ const PublicationsList: FC<Props> = ({ onSelectPublication = () => {} }) => {
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<Input
|
||||
size="tiny"
|
||||
size="small"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
placeholder={'Filter'}
|
||||
value={filterString}
|
||||
|
||||
@@ -62,9 +62,9 @@ const PublicationsTableItem: FC<Props> = ({ table, selectedPublication }) => {
|
||||
{enabledForAllTables ? (
|
||||
// @ts-ignore
|
||||
<Badge
|
||||
type="secondary"
|
||||
className="hover:border-gray-500"
|
||||
style={{ paddingTop: 3, paddingBottom: 3 }}
|
||||
color="scale"
|
||||
// className="hover:border-gray-500"
|
||||
// style={{ paddingTop: 3, paddingBottom: 3 }}
|
||||
>
|
||||
<span>Enabled</span>
|
||||
<span className="hidden lg:inline-block"> for all tables</span>
|
||||
|
||||
@@ -69,7 +69,7 @@ const PublicationsTables: FC<Props> = ({
|
||||
/>
|
||||
<div>
|
||||
<Input
|
||||
size="tiny"
|
||||
size="small"
|
||||
placeholder={'Filter'}
|
||||
value={filterString}
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
|
||||
@@ -39,7 +39,7 @@ const RolesList: FC<Props> = ({ onSelectRole = () => {} }) => {
|
||||
<SparkBar
|
||||
max={x.connection_limit}
|
||||
value={x.active_connections}
|
||||
barClass={'bg-green-500'}
|
||||
barClass={'bg-green-800'}
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Divider, Typography, IconChevronLeft } from '@supabase/ui'
|
||||
import { Button, Typography, IconChevronLeft } from '@supabase/ui'
|
||||
import Divider from 'components/ui/Divider'
|
||||
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
|
||||
@@ -38,40 +39,30 @@ const RolesSettings: FC<Props> = ({ selectedRole, onSelectBack = () => {} }) =>
|
||||
|
||||
<Panel>
|
||||
<Panel.Content className="w-full flex justify-between items-center">
|
||||
<Typography.Text>Super user</Typography.Text>
|
||||
<Typography.Text small code>
|
||||
{selectedRole.is_superuser ? 'true' : 'false'}
|
||||
</Typography.Text>
|
||||
<p className="text-sm">Super user</p>
|
||||
<code className="text-sm">{selectedRole.is_superuser ? 'true' : 'false'}</code>
|
||||
</Panel.Content>
|
||||
<Divider light />
|
||||
<Panel.Content className="w-full flex justify-between items-center">
|
||||
<Typography.Text>User can login</Typography.Text>
|
||||
<Typography.Text small code>
|
||||
{selectedRole.can_login ? 'true' : 'false'}
|
||||
</Typography.Text>
|
||||
<p className="text-sm">User can login</p>
|
||||
<code className="text-sm">{selectedRole.can_login ? 'true' : 'false'}</code>
|
||||
</Panel.Content>
|
||||
<Divider light />
|
||||
<Panel.Content className="w-full flex justify-between items-center">
|
||||
<Typography.Text>User can create databases</Typography.Text>
|
||||
<Typography.Text small code>
|
||||
{selectedRole.can_create_db ? 'true' : 'false'}
|
||||
</Typography.Text>
|
||||
<p className="text-sm">User can create databases</p>
|
||||
<code className="text-sm">{selectedRole.can_create_db ? 'true' : 'false'}</code>
|
||||
</Panel.Content>
|
||||
<Divider light />
|
||||
<Panel.Content className="w-full flex justify-between items-center">
|
||||
<Typography.Text>
|
||||
<p className="text-sm">
|
||||
User can initiate streaming replication and put the system in and out of backup mode
|
||||
</Typography.Text>
|
||||
<Typography.Text small code>
|
||||
{selectedRole.is_replication_role ? 'true' : 'false'}
|
||||
</Typography.Text>
|
||||
</p>
|
||||
<code className="text-sm">{selectedRole.is_replication_role ? 'true' : 'false'}</code>
|
||||
</Panel.Content>
|
||||
<Divider light />
|
||||
<Panel.Content className="w-full flex justify-between items-center">
|
||||
<Typography.Text>User bypasses every row level security policy</Typography.Text>
|
||||
<Typography.Text small code>
|
||||
{selectedRole.can_bypass_rls ? 'true' : 'false'}
|
||||
</Typography.Text>
|
||||
<p className="text-sm">User bypasses every row level security policy</p>
|
||||
<code className="text-sm">{selectedRole.can_bypass_rls ? 'true' : 'false'}</code>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</>
|
||||
|
||||
@@ -33,7 +33,7 @@ const Header: FC<{
|
||||
<div>{leftComponents}</div>
|
||||
<div>
|
||||
<Input
|
||||
size="tiny"
|
||||
size="small"
|
||||
placeholder={filterPlaceholder}
|
||||
value={filterString}
|
||||
onChange={(e: any) => setFilterString(e.target.value)}
|
||||
@@ -47,50 +47,23 @@ const Header: FC<{
|
||||
|
||||
const ColumnList: FC<{
|
||||
selectedTable: any
|
||||
onSelectBack: () => void
|
||||
onAddColumn: () => void
|
||||
onEditColumn: (column: any) => void
|
||||
onSelectBack: () => void
|
||||
onColumnDeleted: () => void
|
||||
onDeleteColumn: (column: any) => void
|
||||
}> = ({
|
||||
selectedTable,
|
||||
onSelectBack = () => {},
|
||||
onAddColumn = () => {},
|
||||
onEditColumn = () => {},
|
||||
onSelectBack = () => {},
|
||||
onColumnDeleted = () => {},
|
||||
onDeleteColumn = () => {},
|
||||
}) => {
|
||||
const { ui, meta } = useStore()
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
const columns =
|
||||
filterString.length === 0
|
||||
? selectedTable.columns
|
||||
: selectedTable.columns.filter((column: any) => column.name.includes(filterString))
|
||||
|
||||
async function onDeleteColumn(column: any) {
|
||||
confirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message: `Are you sure you want to delete the "${column.name}" column? This action cannot be undone.`,
|
||||
onAsyncConfirm: async () => {
|
||||
try {
|
||||
const response: any = await meta.columns.del(column.id)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
} else {
|
||||
onColumnDeleted()
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully removed ${column.name}.`,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete ${column.name}: ${error.message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
@@ -149,25 +122,17 @@ const ColumnList: FC<{
|
||||
</Table.td>
|
||||
<Table.td className="px-4 py-3 pr-2">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
onClick={() => onDeleteColumn(x)}
|
||||
icon={<IconTrash />}
|
||||
style={{ padding: 5 }}
|
||||
type="outline"
|
||||
// tooltip={{
|
||||
// title: 'Delete column',
|
||||
// position: 'top-left',
|
||||
// }}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onEditColumn(x)}
|
||||
icon={<IconEdit3 />}
|
||||
style={{ padding: 5 }}
|
||||
type="outline"
|
||||
// tooltip={{
|
||||
// title: 'Edit Details',
|
||||
// position: 'top-left',
|
||||
// }}
|
||||
type="text"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onDeleteColumn(x)}
|
||||
icon={<IconTrash />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
|
||||
const Header: FC<{
|
||||
filterString: string
|
||||
@@ -33,7 +32,7 @@ const Header: FC<{
|
||||
<div>{leftComponents}</div>
|
||||
<div>
|
||||
<Input
|
||||
size="tiny"
|
||||
size="small"
|
||||
placeholder={filterPlaceholder}
|
||||
value={filterString}
|
||||
onChange={(e: any) => setFilterString(e.target.value)}
|
||||
@@ -48,134 +47,109 @@ const Header: FC<{
|
||||
const TableList: FC<{
|
||||
onAddTable: () => void
|
||||
onEditTable: (table: any) => void
|
||||
onDeleteTable: (table: any) => void
|
||||
onOpenTable: (table: any) => void
|
||||
}> = observer(({ onAddTable = () => {}, onEditTable = () => {}, onOpenTable = () => {} }) => {
|
||||
const { ui, meta } = useStore()
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
const tables =
|
||||
filterString.length === 0
|
||||
? meta.tables.list((table: any) => table.schema === 'public')
|
||||
: meta.tables.list(
|
||||
(table: any) => table.schema === 'public' && table.name.includes(filterString)
|
||||
)
|
||||
}> = observer(
|
||||
({
|
||||
onAddTable = () => {},
|
||||
onEditTable = () => {},
|
||||
onDeleteTable = () => {},
|
||||
onOpenTable = () => {},
|
||||
}) => {
|
||||
const { meta } = useStore()
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
const tables =
|
||||
filterString.length === 0
|
||||
? meta.tables.list((table: any) => table.schema === 'public')
|
||||
: meta.tables.list(
|
||||
(table: any) => table.schema === 'public' && table.name.includes(filterString)
|
||||
)
|
||||
|
||||
async function onDelete(table: any) {
|
||||
confirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message: `Are you sure you want to delete "${table.name}" table? This action cannot be undone.`,
|
||||
onAsyncConfirm: async () => {
|
||||
try {
|
||||
const response: any = await meta.tables.del(table.id)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully removed ${table.name}.`,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete ${table.name}: ${error.message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{/* @ts-ignore */}
|
||||
<Header
|
||||
filterString={filterString}
|
||||
setFilterString={setFilterString}
|
||||
rightComponents={
|
||||
<Button icon={<IconPlus />} onClick={() => onAddTable()}>
|
||||
New
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full my-4">
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="name">Name</Table.th>,
|
||||
<Table.th key="schema">Schema</Table.th>,
|
||||
<Table.th key="description" className="hidden lg:table-cell">
|
||||
Description
|
||||
</Table.th>,
|
||||
<Table.th key="rows" className="hidden xl:table-cell">
|
||||
Rows (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="size" className="hidden xl:table-cell">
|
||||
Size (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="buttons"></Table.th>,
|
||||
]}
|
||||
body={tables.map((x: any, i: any) => (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.td>
|
||||
<Typography.Text>{x.name}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>{x.schema}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className=" truncate max-w-sm hidden lg:table-cell">
|
||||
<Typography.Text>{x.comment}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className=" hidden xl:table-cell">
|
||||
<Typography.Text small code>
|
||||
{x.live_rows_estimate ?? x.live_row_count}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className=" hidden xl:table-cell">
|
||||
<Typography.Text small code>
|
||||
{x.size}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
iconRight={<IconColumns />}
|
||||
type="default"
|
||||
className="hover:border-gray-500 whitespace-nowrap"
|
||||
style={{ paddingTop: 3, paddingBottom: 3 }}
|
||||
onClick={() => {
|
||||
onOpenTable(x)
|
||||
}}
|
||||
>
|
||||
{x.columns.length} columns
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onEditTable(x)}
|
||||
icon={<IconEdit3 />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onDeleteTable(x)}
|
||||
icon={<IconTrash />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{/* @ts-ignore */}
|
||||
<Header
|
||||
filterString={filterString}
|
||||
setFilterString={setFilterString}
|
||||
rightComponents={
|
||||
<Button icon={<IconPlus />} onClick={() => onAddTable()}>
|
||||
New
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full my-4">
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="name">Name</Table.th>,
|
||||
<Table.th key="schema">Schema</Table.th>,
|
||||
<Table.th key="description" className="hidden lg:table-cell">
|
||||
Description
|
||||
</Table.th>,
|
||||
<Table.th key="rows" className="hidden xl:table-cell">
|
||||
Rows (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="size" className="hidden xl:table-cell">
|
||||
Size (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="buttons"></Table.th>,
|
||||
]}
|
||||
body={tables.map((x: any, i: any) => (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.td>
|
||||
<Typography.Text>{x.name}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<Typography.Text>{x.schema}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className=" truncate max-w-sm hidden lg:table-cell">
|
||||
<Typography.Text>{x.comment}</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className=" hidden xl:table-cell">
|
||||
<Typography.Text small code>
|
||||
{x.live_rows_estimate ?? x.live_row_count}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td className=" hidden xl:table-cell">
|
||||
<Typography.Text small code>
|
||||
{x.size}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
iconRight={<IconColumns />}
|
||||
type="default"
|
||||
className="hover:border-gray-500 whitespace-nowrap"
|
||||
style={{ paddingTop: 3, paddingBottom: 3 }}
|
||||
onClick={() => {
|
||||
onOpenTable(x)
|
||||
}}
|
||||
>
|
||||
{x.columns.length} columns
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onEditTable(x)}
|
||||
icon={<IconEdit3 />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
// tooltip={{
|
||||
// title: 'Edit Details',
|
||||
// position: 'top-left',
|
||||
// }}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onDelete(x)}
|
||||
icon={<IconTrash />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
// tooltip={{
|
||||
// title: 'Delete table',
|
||||
// position: 'top-left',
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
export default TableList
|
||||
@@ -6,14 +6,15 @@ import { Transition } from '@headlessui/react'
|
||||
import {
|
||||
Button,
|
||||
IconChevronDown,
|
||||
IconHelpCircle,
|
||||
IconTerminal,
|
||||
IconTool,
|
||||
SidePanel,
|
||||
Typography,
|
||||
} from '@supabase/ui'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import SqlEditor from 'components/to-be-cleaned/SqlEditor'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
interface IChooseFunctionFormStore {
|
||||
functionSchemas: string[]
|
||||
@@ -55,16 +56,16 @@ const ChooseFunctionForm: React.FC<ChooseFunctionFormProps> = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
title="Pick a function"
|
||||
size="large"
|
||||
header="Pick a function"
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(!visible)}
|
||||
className="hooks-sidepanel"
|
||||
>
|
||||
{hasPublicSchemaFunctions ? (
|
||||
<ChooseFunctionFormContext.Provider value={_localState}>
|
||||
<NoticeBox />
|
||||
<div className="space-y-6">
|
||||
<NoticeBox />
|
||||
{_localState.functionSchemas.map((schema: string) => (
|
||||
<SchemaFunctionGroup key={schema} schema={schema} selectFunction={selectFunction} />
|
||||
))}
|
||||
@@ -83,36 +84,22 @@ const NoticeBox: React.FC = ({}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
return (
|
||||
<div className="px-6 mb-8">
|
||||
<div className="block w-full bg-white bg-opacity-5 p-3 px-6 border border-white border-opacity-20 rounded">
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconTool className="text-white" size="large" />
|
||||
</div>
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div>
|
||||
<Typography.Text>
|
||||
Only functions that return a trigger will be displayed below
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Typography.Text type="secondary">
|
||||
You can make functions by using the Database Functions
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="secondary"
|
||||
className="flex-grow"
|
||||
onClick={() => {
|
||||
router.push(`/project/${ref}/database/functions`)
|
||||
}}
|
||||
>
|
||||
Go to Functions
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6">
|
||||
<InformationBox
|
||||
icon={<IconHelpCircle size="large" strokeWidth={1.5} />}
|
||||
title="Only functions that return a trigger will be displayed below"
|
||||
description={`You can make functions by using the Database Functions`}
|
||||
button={
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={() => {
|
||||
router.push(`/project/${ref}/database/functions`)
|
||||
}}
|
||||
>
|
||||
Go to Functions
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -130,9 +117,9 @@ const NoFunctionsState: React.FC = ({}) => {
|
||||
router.push(`/project/${ref}/database/functions`)
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary">
|
||||
<p className="text-sm text-scale-1100">
|
||||
You will need to create a trigger based function before you can add it to your trigger.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</ProductEmptyState>
|
||||
)
|
||||
}
|
||||
@@ -148,7 +135,7 @@ const SchemaFunctionGroup: React.FC<SchemaFunctionGroupProps> = observer(
|
||||
const _functions = _pageState!.triggerFunctions.filter((x) => x.schema == schema)
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="z-10 sticky top-0 backdrop-filter backdrop-blur px-6 flex items-center space-x-1">
|
||||
<div className="sticky top-0 backdrop-filter backdrop-blur px-6 flex items-center space-x-1">
|
||||
<Typography.Title level={5} className="opacity-50">
|
||||
schema
|
||||
</Typography.Title>
|
||||
@@ -186,8 +173,8 @@ const Function: React.FC<FunctionProps> = ({ id, completeStatement, name, onClic
|
||||
>
|
||||
<div className="flex items-center space-x-3 justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-600 p-1 flex items-center justify-center rounded text-typography-body-dark ">
|
||||
<IconTerminal size="small" strokeWidth={2} />
|
||||
<div className="bg-scale-1200 p-1 flex items-center justify-center rounded text-scale-100 ">
|
||||
<IconTerminal strokeWidth={2} size={14} />
|
||||
</div>
|
||||
<Typography.Title level={5} className="mb-0">
|
||||
{name}
|
||||
|
||||
@@ -5,15 +5,14 @@ import { isEmpty, mapValues, has, without, union } from 'lodash'
|
||||
import {
|
||||
Input,
|
||||
SidePanel,
|
||||
Divider,
|
||||
Checkbox,
|
||||
Listbox,
|
||||
Typography,
|
||||
IconPlayCircle,
|
||||
IconPauseCircle,
|
||||
IconTerminal,
|
||||
IconTool,
|
||||
Badge,
|
||||
Button,
|
||||
} from '@supabase/ui'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -325,13 +324,10 @@ const CreateTrigger: FC<CreateTriggerProps> = ({ trigger, visible, setVisible })
|
||||
return (
|
||||
<>
|
||||
<SidePanel
|
||||
wide
|
||||
size="large"
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(!visible)}
|
||||
// @ts-ignore
|
||||
contentStyle={{ padding: 0 }}
|
||||
footerBackground={true}
|
||||
title={_localState.title}
|
||||
header={_localState.title}
|
||||
hideFooter={!hasPublicTables}
|
||||
className={
|
||||
_localState.chooseFunctionFormVisible
|
||||
@@ -342,9 +338,9 @@ const CreateTrigger: FC<CreateTriggerProps> = ({ trigger, visible, setVisible })
|
||||
onConfirm={handleSubmit}
|
||||
>
|
||||
{hasPublicTables ? (
|
||||
<div className="pb-8">
|
||||
<div className="">
|
||||
<CreateTriggerContext.Provider value={_localState}>
|
||||
<div className="space-y-10 mt-4">
|
||||
<div className="space-y-10 my-6">
|
||||
{_localState.isEditing ? (
|
||||
<div className="px-6 space-y-6">
|
||||
<InputName />
|
||||
@@ -355,7 +351,7 @@ const CreateTrigger: FC<CreateTriggerProps> = ({ trigger, visible, setVisible })
|
||||
<div className="px-6">
|
||||
<InputName />
|
||||
</div>
|
||||
<Divider light />
|
||||
<SidePanel.Seperator />
|
||||
<div className="px-6 space-y-12">
|
||||
<Typography.Title level={5}>Conditions to fire trigger</Typography.Title>
|
||||
<ListboxTable />
|
||||
@@ -363,7 +359,7 @@ const CreateTrigger: FC<CreateTriggerProps> = ({ trigger, visible, setVisible })
|
||||
<ListboxActivation />
|
||||
<SelectOrientation />
|
||||
</div>
|
||||
<Divider light />
|
||||
<SidePanel.Seperator />
|
||||
<FunctionForm />
|
||||
</>
|
||||
)}
|
||||
@@ -397,9 +393,9 @@ const NoTableState: FC = ({}) => {
|
||||
router.push(`/project/${ref}/database/tables`)
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary">
|
||||
<p className="text-sm text-scale-1100">
|
||||
You will need to create a table first before you can make a trigger
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</ProductEmptyState>
|
||||
)
|
||||
}
|
||||
@@ -448,52 +444,54 @@ const SelectEnabledMode: FC = observer(({}) => {
|
||||
<Listbox.Option
|
||||
addOnBefore={({ active, selected }: any) => {
|
||||
return (
|
||||
<div className="bg-green-500 border border-green-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
<div className="bg-green-900 border border-green-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
)
|
||||
}}
|
||||
value="ORIGIN"
|
||||
label="Origin"
|
||||
>
|
||||
Origin
|
||||
<span className="opacity-50 block">This is a default behaviour</span>
|
||||
<span className="text-scale-900 block">This is a default behaviour</span>
|
||||
</Listbox.Option>
|
||||
<Listbox.Option
|
||||
addOnBefore={({ active, selected }: any) => {
|
||||
return (
|
||||
<div className="bg-green-500 border border-green-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
<div className="bg-green-900 border border-green-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
)
|
||||
}}
|
||||
value="REPLICA"
|
||||
label="Replica"
|
||||
>
|
||||
Replica
|
||||
<span className="opacity-50 block">Will only fire if the session is in “replica” mode</span>
|
||||
<span className="text-scale-900 block">
|
||||
Will only fire if the session is in “replica” mode
|
||||
</span>
|
||||
</Listbox.Option>
|
||||
<Listbox.Option
|
||||
addOnBefore={({ active, selected }: any) => {
|
||||
return (
|
||||
<div className="bg-green-500 border border-green-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
<div className="bg-green-900 border border-green-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
)
|
||||
}}
|
||||
value="ALWAYS"
|
||||
label="Always"
|
||||
>
|
||||
Always
|
||||
<span className="opacity-50 block">
|
||||
<span className="text-scale-900 block">
|
||||
Will fire regardless of the current replication role
|
||||
</span>
|
||||
</Listbox.Option>
|
||||
<Listbox.Option
|
||||
addOnBefore={({ active, selected }: any) => {
|
||||
return (
|
||||
<div className="bg-red-500 border border-red-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
<div className="bg-red-900 border border-red-700 shadow-sm w-3 h-3 rounded-full"></div>
|
||||
)
|
||||
}}
|
||||
value="DISABLED"
|
||||
label="Disabled"
|
||||
>
|
||||
Disabled
|
||||
<span className="opacity-50 block">Will not fire</span>
|
||||
<span className="text-scale-900 block">Will not fire</span>
|
||||
</Listbox.Option>
|
||||
</Listbox>
|
||||
)
|
||||
@@ -518,11 +516,11 @@ const SelectOrientation: FC = observer(({}) => {
|
||||
>
|
||||
<Listbox.Option value="ROW" label="Row">
|
||||
Row
|
||||
<span className="opacity-50 block">fires once for each processed row</span>
|
||||
<span className="text-scale-900 block">fires once for each processed row</span>
|
||||
</Listbox.Option>
|
||||
<Listbox.Option value="STATEMENT" label="Statement">
|
||||
Statement
|
||||
<span className="opacity-50 block">fires once for each statement</span>
|
||||
<span className="text-scale-900 block">fires once for each statement</span>
|
||||
</Listbox.Option>
|
||||
</Listbox>
|
||||
)
|
||||
@@ -566,7 +564,7 @@ const ListboxTable: FC = observer(({}) => {
|
||||
value={x.id}
|
||||
label={x.name}
|
||||
addOnBefore={() => (
|
||||
<div className="bg-green-600 p-1 flex items-center justify-center rounded text-typography-body-dark ">
|
||||
<div className="bg-scale-1200 p-1 flex items-center justify-center rounded text-scale-100 ">
|
||||
<SVG
|
||||
src={'/img/table-editor.svg'}
|
||||
style={{ width: `16px`, height: `16px`, strokeWidth: '1px' }}
|
||||
@@ -579,8 +577,7 @@ const ListboxTable: FC = observer(({}) => {
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<Typography.Text>{x.name}</Typography.Text>
|
||||
<Divider light type="vertical" />
|
||||
<Typography.Text type="secondary" className="opacity-50" small>
|
||||
<Typography.Text type="secondary" className="text-scale-900" small>
|
||||
{x.schema}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
@@ -617,19 +614,19 @@ const CheckboxEvents: FC = observer(({}) => {
|
||||
>
|
||||
<Checkbox
|
||||
value="INSERT"
|
||||
label="INSERT"
|
||||
label="Insert"
|
||||
description={'Any insert operation on the table'}
|
||||
checked={_localState!.formState.events.value.includes('INSERT')}
|
||||
/>
|
||||
<Checkbox
|
||||
value="UPDATE"
|
||||
label="UPDATE"
|
||||
label="Update"
|
||||
description="Any update operation, of any column in the table"
|
||||
checked={_localState!.formState.events.value.includes('UPDATE')}
|
||||
/>
|
||||
<Checkbox
|
||||
value="DELETE"
|
||||
label="DELETE"
|
||||
label="Delete"
|
||||
description="Any deletion of a record"
|
||||
checked={_localState!.formState.events.value.includes('DELETE')}
|
||||
/>
|
||||
@@ -660,14 +657,16 @@ const ListboxActivation: FC = observer(({}) => {
|
||||
value={'BEFORE'}
|
||||
label={'Before the event'}
|
||||
addOnBefore={() => (
|
||||
<div className="bg-green-600 p-1 flex items-center justify-center rounded text-typography-body-dark ">
|
||||
<div className="bg-scale-1200 p-1 flex items-center justify-center rounded text-scale-100 ">
|
||||
<IconPauseCircle strokeWidth={2} size="small" />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span>{'before'}</span>
|
||||
<span className="opacity-50 block">Trigger fires before the operation is attempted</span>
|
||||
<span className="text-scale-900 block">
|
||||
Trigger fires before the operation is attempted
|
||||
</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
<Listbox.Option
|
||||
@@ -675,14 +674,16 @@ const ListboxActivation: FC = observer(({}) => {
|
||||
value={'AFTER'}
|
||||
label={'After the event'}
|
||||
addOnBefore={() => (
|
||||
<div className="bg-green-600 p-1 flex items-center justify-center rounded text-typography-body-dark ">
|
||||
<div className="bg-green-1200 p-1 flex items-center justify-center rounded text-scale-100 ">
|
||||
<IconPlayCircle strokeWidth={2} size="small" />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span>{'after'}</span>
|
||||
<span className="opacity-50 block">Trigger fires after the operation has completed</span>
|
||||
<span className="text-scale-900 block">
|
||||
Trigger fires after the operation has completed
|
||||
</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
</Listbox>
|
||||
@@ -714,19 +715,23 @@ const FunctionEmpty: FC = observer(({}) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => _localState!.setChooseFunctionFormVisible(true)}
|
||||
className="w-full relative transition-shadow shadow-sm
|
||||
border-2 border-dashed dark:border-dark rounded hover:border-gray-300
|
||||
px-6 py-4
|
||||
hover:bg-bg-alt-light dark:hover:bg-bg-alt-dark hover:bg-opacity-25 hover:shadow-md cursor-pointer
|
||||
flex space-x-3 items-center"
|
||||
className="w-full relative
|
||||
transition-all
|
||||
|
||||
shadow-sm
|
||||
border bg-scale-200 dark:bg-scale-400
|
||||
border-scale-600
|
||||
rounded
|
||||
hover:border-scale-700
|
||||
hover:bg-scale-300 dark:hover:bg-scale-500
|
||||
px-5 py-1
|
||||
|
||||
"
|
||||
>
|
||||
<FormEmptyBox
|
||||
icon={<IconTerminal size={21} strokeWidth={2} />}
|
||||
icon={<IconTerminal size={14} strokeWidth={2} />}
|
||||
text="Choose a function to trigger"
|
||||
/>
|
||||
<div className="form-info-panel__edit-icon transition-all opacity-0 absolute right-3 top-3 mt-0 bg-gray-600 w-8 h-8 rounded-full flex items-center justify-center text-white">
|
||||
<IconTool size="small" />
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
@@ -736,34 +741,38 @@ const FunctionWithArguments: FC = observer(({}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => _localState!.setChooseFunctionFormVisible(true)}
|
||||
className="bg-bg-alt-light dark:bg-bg-alt-dark
|
||||
<div
|
||||
className="
|
||||
|
||||
|
||||
w-full
|
||||
form-info-panel
|
||||
|
||||
relative
|
||||
transition-shadow shadow-sm
|
||||
border dark:border-dark rounded hover:border-gray-300
|
||||
px-6 py-4
|
||||
hover:bg-gray-100 hover:bg-opacity-25 hover:shadow-md cursor-pointer
|
||||
flex space-x-3 items-center"
|
||||
border border-scale-200 dark:border-scale-500
|
||||
rounded
|
||||
|
||||
|
||||
px-5 py-4
|
||||
flex space-x-3 items-center justify-between"
|
||||
>
|
||||
<div className="flex rounded-full w-8 h-8 bg-green-700 bg-opacity-10 items-center justify-center">
|
||||
<IconTerminal size="small" strokeWidth={2} />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<Typography.Text type="secondary">
|
||||
{_localState!.formState.functionName.value}
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Badge>{_localState!.formState.functionSchema.value}</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex rounded w-6 h-6 bg-scale-1200 text-scale-100 focus-within:bg-opacity-10 items-center justify-center">
|
||||
<IconTerminal size="small" strokeWidth={2} width={14} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Typography.Text type="secondary">
|
||||
{_localState!.formState.functionName.value}
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Badge>{_localState!.formState.functionSchema.value}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-info-panel__edit-icon transition-all opacity-0 absolute right-3 top-3 mt-0 bg-gray-600 w-8 h-8 rounded-full flex items-center justify-center text-white">
|
||||
<IconTool size="small" />
|
||||
</div>
|
||||
</button>
|
||||
<Button type="default" onClick={() => _localState!.setChooseFunctionFormVisible(true)}>
|
||||
Change function
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -19,25 +19,25 @@ const ClientLibrary: FC<Props> = ({ language, releaseState, officialSupport, doc
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Typography.Title level={5}>
|
||||
<h5 className="text-scale-1200 text-base flex items-center gap-2">
|
||||
{language} {releaseState && <Badge color="yellow">{`Public ${releaseState}`}</Badge>}
|
||||
</Typography.Title>
|
||||
<Typography.Text>
|
||||
</h5>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
{officialSupport
|
||||
? 'This library is officially supported'
|
||||
: 'This library is community supported'}
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-1">
|
||||
{docsUrl && (
|
||||
<a href={docsUrl} target="_blank">
|
||||
<Button icon={<IconBookOpen />} type="outline">
|
||||
<Button icon={<IconBookOpen />} type="default">
|
||||
Docs
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
<a href={gitUrl} target="_blank">
|
||||
<Button icon={<IconGitHub />} type="outline">
|
||||
<Button icon={<IconGitHub />} type="default">
|
||||
See GitHub
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
@@ -13,26 +13,40 @@ const ExampleProject: FC<Props> = ({ framework, title, description, url }) => {
|
||||
<a href={url}>
|
||||
<div
|
||||
className={[
|
||||
'bg-panel-header-light dark:bg-panel-header-dark hover:bg-bg-alt-light dark:hover:bg-bg-alt-dark border',
|
||||
'border-border-secondary-light dark:border-border-secondary-dark hover:border-border-secondary-hover-light',
|
||||
'dark:hover:border-border-secondary-hover-dark rounded-md p-4 flex flex-row h-32 hover:border-gray-300',
|
||||
'group relative',
|
||||
'bg-panel-header-light dark:bg-panel-header-dark hover:bg-panel-border-light dark:hover:bg-panel-border-dark border',
|
||||
'border-panel-border-light dark:border-panel-border-dark hover:border-panel-border-hover-light',
|
||||
'dark:hover:border-panel-border-hover-dark rounded-md p-4 flex flex-row h-32 hover:border-gray-300',
|
||||
'transition ease-in-out duration-150',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex flex-col mr-4">
|
||||
<img
|
||||
className="
|
||||
transition-all
|
||||
group-hover:scale-110
|
||||
"
|
||||
src={`/img/libraries/${framework.toLowerCase()}-icon.svg`}
|
||||
alt={`${framework} logo`}
|
||||
width="26"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4 w-4/5">
|
||||
<div>
|
||||
<Typography.Title level={5}>{title}</Typography.Title>
|
||||
<Typography.Text type="secondary">{description}</Typography.Text>
|
||||
</div>
|
||||
<div className="space-y-2 w-4/5">
|
||||
<h5 className="text-scale-1200">{title}</h5>
|
||||
<p className="text-scale-1000 text-sm">{description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="
|
||||
absolute
|
||||
right-4
|
||||
top-3
|
||||
text-scale-900
|
||||
transition-all
|
||||
duration-200
|
||||
group-hover:right-3
|
||||
group-hover:text-scale-1200
|
||||
"
|
||||
>
|
||||
<IconChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,38 +46,35 @@ const Landing = () => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:block md:ml-10 md:pr-4">
|
||||
<Typography.Text className="ml-8 font-medium hover:text-gray-300 focus:outline-none focus:text-gray-300 transition duration-150 ease-in-out">
|
||||
<Link href="https://supabase.com/docs">Documentation</Link>
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="success"
|
||||
className="ml-8 font-medium hover:text-gray-300 focus:outline-none focus:text-gray-300 transition duration-150 ease-in-out"
|
||||
<div className="hidden md:ml-10 md:pr-4 md:flex items-center gap-3">
|
||||
<a
|
||||
href="https://supabase.com/docs"
|
||||
className="text-sm text-scale-1100 hover:text-scale-1200 transition-colors"
|
||||
>
|
||||
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
|
||||
Sign In With GitHub
|
||||
</Button>
|
||||
</Typography.Text>
|
||||
Documentation
|
||||
</a>
|
||||
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
|
||||
Sign In with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center justify-center mx-auto h-full max-w-screen-xl px-8">
|
||||
<div className="sm:text-center">
|
||||
<Typography.Title level={2}>
|
||||
Give your database <span className="text-green-500">superpowers</span>
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
<p className="mt-3 text-lg sm:mt-5 sm:max-w-2xl sm:mx-auto md:mt-5 lg:mx-0">
|
||||
Create a backend in less than 2 minutes. Start your project with a Postgres Database,
|
||||
Authentication, instant APIs, and realtime subscriptions.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<div className="mt-5 sm:mt-8 sm:justify-center space-x-3 flex items-center">
|
||||
<Button onClick={handleGithubSignIn} size="large" icon={<IconGitHub />}>
|
||||
Sign In With GitHub
|
||||
<h1 className="text-3xl">
|
||||
Give Your Database <span className="text-brand-900">Superpowers</span>
|
||||
</h1>
|
||||
<p className="text-scale-1100 text-base sm:max-w-2xl sm:mx-auto md:mt-5 mb-10">
|
||||
Create a backend in less than 2 minutes. Start your project with a Postgres Database,
|
||||
Authentication, instant APIs, and realtime subscriptions.
|
||||
</p>
|
||||
|
||||
<div className="sm:justify-center flex items-center gap-2">
|
||||
<Button onClick={handleGithubSignIn} size="medium" icon={<IconGitHub />}>
|
||||
Sign In with GitHub
|
||||
</Button>
|
||||
<Link href="https://supabase.com/docs">
|
||||
<Button size="large" type="default">
|
||||
<Button size="medium" type="default">
|
||||
Docs
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -15,17 +15,11 @@ const GetStartedPanel: FC<Props> = ({}) => {
|
||||
<div>
|
||||
<div className="flex flex-col justify-between h-full space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Typography.Title level={4} className="m-0">
|
||||
Welcome to your new project
|
||||
</Typography.Title>
|
||||
<div className="lg:max-w-lg">
|
||||
<Typography.Text className="block" type="secondary">
|
||||
<p>
|
||||
Your project has been deployed on its own instance, with its own url and API all
|
||||
set up and ready to use.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<h3 className="text-xl text-scale-1200">Welcome to your new project</h3>
|
||||
<p className="lg:max-w-lg text-base text-scale-1000">
|
||||
Your project has been deployed on its own instance, with its own url and API all set
|
||||
up and ready to use.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,17 +28,15 @@ const GetStartedPanel: FC<Props> = ({}) => {
|
||||
<Panel>
|
||||
<Panel.Content className=" flex flex-col space-y-4 lg:h-40">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Typography.Text>
|
||||
<IconDatabase size="medium" />
|
||||
</Typography.Text>
|
||||
<Typography.Title level={5} className="mb-0">
|
||||
Database
|
||||
</Typography.Title>
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<IconDatabase strokeWidth={2} size={16} />
|
||||
</div>
|
||||
<h5>Database</h5>
|
||||
</div>
|
||||
<div className="flex flex-grow">
|
||||
<Typography.Text type="secondary" className="opacity-50">
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Supabase is built on top of Postgres, an extremely scalable Relational Database.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Link href={`/project/${ref}/editor`}>
|
||||
@@ -67,17 +59,15 @@ const GetStartedPanel: FC<Props> = ({}) => {
|
||||
<Panel className="">
|
||||
<Panel.Content className=" flex flex-col space-y-4 lg:h-40">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Typography.Text>
|
||||
<IconKey size="medium" />
|
||||
</Typography.Text>
|
||||
<Typography.Title level={5} className="mb-0">
|
||||
Auth
|
||||
</Typography.Title>
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<IconKey strokeWidth={2} size={16} />
|
||||
</div>
|
||||
<h5>Auth</h5>
|
||||
</div>
|
||||
<div className="flex flex-grow">
|
||||
<Typography.Text type="secondary" className="opacity-50">
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Supabase makes it simple to manage your users.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Link href={`/project/${ref}/auth/users`}>
|
||||
@@ -95,17 +85,15 @@ const GetStartedPanel: FC<Props> = ({}) => {
|
||||
<Panel>
|
||||
<Panel.Content className=" flex flex-col space-y-4 lg:h-40">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Typography.Text>
|
||||
<IconArchive size="medium" />
|
||||
</Typography.Text>
|
||||
<Typography.Title level={5} className="mb-0">
|
||||
Storage
|
||||
</Typography.Title>
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<IconArchive strokeWidth={2} size={16} />
|
||||
</div>
|
||||
<h5>Storage</h5>
|
||||
</div>
|
||||
<div className="flex flex-grow">
|
||||
<Typography.Text type="secondary" className="opacity-50">
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Store and serve large files from multiple buckets.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Link href={`/project/${ref}/storage/buckets`}>
|
||||
|
||||
@@ -16,17 +16,11 @@ const NewProjectPanel: FC<Props> = ({}) => (
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<div className="space-y-2">
|
||||
<Typography.Title level={4} className="m-0">
|
||||
Connecting to your new project
|
||||
</Typography.Title>
|
||||
<div className="lg:max-w-sm">
|
||||
<Typography.Text className="block" type="secondary">
|
||||
<p>
|
||||
Your project has API keys for interacting with the database via Supabase client
|
||||
libraries.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<h3 className="text-xl text-scale-1200">Connecting to your new project</h3>
|
||||
|
||||
<p className="lg:max-w-sm text-scale-1000">
|
||||
Your project has API keys for interacting with the database via Supabase client libraries.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8">
|
||||
|
||||
@@ -1,34 +1,71 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Typography } from '@supabase/ui'
|
||||
|
||||
import { Badge, Button, IconClock } from '@supabase/ui'
|
||||
import { Project } from 'types'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
paused: boolean
|
||||
onSelectRestore?: () => void
|
||||
onSelectDelete?: () => void
|
||||
rewriteHref?: string
|
||||
}
|
||||
|
||||
const ProjectCard: FC<Props> = ({ project, rewriteHref }) => {
|
||||
const ProjectCard: FC<Props> = ({
|
||||
project,
|
||||
paused,
|
||||
onSelectRestore,
|
||||
onSelectDelete,
|
||||
rewriteHref,
|
||||
}) => {
|
||||
const { name, ref: projectRef, status } = project
|
||||
const desc = `${project.cloud_provider} | ${project.region}`
|
||||
|
||||
return (
|
||||
<li className="col-span-1 flex shadow-sm rounded-md">
|
||||
<Link href={rewriteHref ?? `/project/${projectRef}`}>
|
||||
<a className="w-full col-span-3 md:col-span-1">
|
||||
<div
|
||||
className="bg-panel-header-light dark:bg-panel-header-dark hover:bg-bg-alt-light
|
||||
dark:hover:bg-bg-alt-dark border border-border-secondary-light
|
||||
dark:border-border-secondary-dark hover:border-border-secondary-hover-light
|
||||
dark:hover:border-border-secondary-hover-dark p-4 h-32 rounded transition
|
||||
ease-in-out duration-150 flex flex-col justify-between"
|
||||
>
|
||||
<Typography.Title level={4}>{name}</Typography.Title>
|
||||
<div className="lowercase">
|
||||
<Typography.Text>{desc}</Typography.Text>
|
||||
</div>
|
||||
<li className="col-span-1">
|
||||
<CardButton
|
||||
linkHref={rewriteHref ? rewriteHref : paused ? '' : `/project/${projectRef}`}
|
||||
title={
|
||||
<div className="flex items-center gap-3 w-full">
|
||||
<span>{name}</span>
|
||||
{paused && (
|
||||
<Badge color="scale">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconClock size={14} strokeWidth={2} />
|
||||
<span>Project paused</span>
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
description={''}
|
||||
footer={
|
||||
<div className="lowercase flex items-center justify-between">
|
||||
<span className="text-sm text-scale-900">{desc}</span>
|
||||
{paused && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
onClick={() => onSelectRestore && onSelectRestore()}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
onClick={() => onSelectDelete && onSelectDelete()}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
></CardButton>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,14 +13,12 @@ import PausedProjectCard from './PausedProjectCard'
|
||||
interface Props {
|
||||
onSelectRestore: (project: Project) => void
|
||||
onSelectDelete: (project: Project) => void
|
||||
showInactiveProjects?: boolean
|
||||
rewriteHref?: (projectRef: string) => string
|
||||
}
|
||||
|
||||
const ProjectList: FC<Props> = ({
|
||||
onSelectRestore = () => {},
|
||||
onSelectDelete = () => {},
|
||||
showInactiveProjects = true,
|
||||
rewriteHref,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
@@ -39,13 +37,17 @@ const ProjectList: FC<Props> = ({
|
||||
return (
|
||||
<div className="space-y-3" key={makeRandomString(5)}>
|
||||
<Loading active={isLoading}>
|
||||
<Typography.Title level={4}>{name}</Typography.Title>
|
||||
<h4 className="text-lg">{name}</h4>
|
||||
</Loading>
|
||||
<ul className="grid grid-cols-1 gap-4 mx-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<ul className="grid gap-4 mx-auto grid-cols-1 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ">
|
||||
{!isLoading && isEmpty && (
|
||||
<div className="max-w-4xl text-center col-span-4 space-y-4 border-2 border-gray-300 border-dashed rounded-lg p-6">
|
||||
<Typography.Title level={5}>No projects.</Typography.Title>
|
||||
<Typography.Text>Get started by creating a new project.</Typography.Text>
|
||||
<div className="space-y-1">
|
||||
<p>No projects</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Get started by creating a new project.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={() => router.push(`/new/${slug}`)} icon={<IconPlus />}>
|
||||
New Project
|
||||
@@ -53,24 +55,15 @@ const ProjectList: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sortedProjects?.map((project: Project) =>
|
||||
project.status === PROJECT_STATUS.INACTIVE ? (
|
||||
showInactiveProjects && (
|
||||
<PausedProjectCard
|
||||
key={makeRandomString(5)}
|
||||
project={project}
|
||||
onSelectDelete={() => onSelectDelete(project)}
|
||||
onSelectRestore={() => onSelectRestore(project)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<ProjectCard
|
||||
key={makeRandomString(5)}
|
||||
project={project}
|
||||
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{sortedProjects?.map((project: Project) => (
|
||||
<ProjectCard
|
||||
paused={project.status === PROJECT_STATUS.INACTIVE}
|
||||
key={makeRandomString(5)}
|
||||
project={project}
|
||||
onSelectDelete={() => onSelectDelete(project)}
|
||||
onSelectRestore={() => onSelectRestore(project)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -84,37 +84,42 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
}
|
||||
return (
|
||||
<div className="mx-6 space-y-6">
|
||||
<div className="flex flex-row justify-between align-center w-full">
|
||||
<Typography.Title level={4}>Statistics for past {selectedInterval.label}</Typography.Title>
|
||||
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{logsUsageChartIntervals && (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
align="start"
|
||||
overlay={
|
||||
<Dropdown.RadioGroup value={interval} onChange={setInterval}>
|
||||
{CHART_INTERVALS.map((i) => (
|
||||
<Dropdown.Radio key={i.key} value={i.key}>
|
||||
<Typography.Text>{i.label}</Typography.Text>
|
||||
{i.label}
|
||||
</Dropdown.Radio>
|
||||
))}
|
||||
</Dropdown.RadioGroup>
|
||||
}
|
||||
>
|
||||
<Button type="text" iconRight={<IconChevronDown />}>
|
||||
<Button as="span" type="default" iconRight={<IconChevronDown />}>
|
||||
{selectedInterval.label}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
<span className="text-scale-1000 text-xs">
|
||||
Statistics for past {selectedInterval.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="">
|
||||
{startDate && endDate && (
|
||||
<>
|
||||
<div className="grid lg:grid-cols-4 lg:gap-8">
|
||||
<div className="grid lg:grid-cols-2 lg:gap-8">
|
||||
<Panel key="database-chart">
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader
|
||||
icon={<IconDatabase size="small" />}
|
||||
icon={
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<IconDatabase strokeWidth={2} size={16} />
|
||||
</div>
|
||||
}
|
||||
title="Database"
|
||||
href={`/project/${ref}/editor`}
|
||||
/>
|
||||
@@ -129,7 +134,6 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
customDateFormat={datetimeFormat}
|
||||
data={charts}
|
||||
isLoading={!charts && !error ? true : false}
|
||||
onBarClick={(v) => handleBarClick(v, '/rest')}
|
||||
/>
|
||||
<ProjectUsageMinimal
|
||||
projectRef={project.ref}
|
||||
@@ -141,7 +145,11 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
<Panel key="auth-chart">
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader
|
||||
icon={<IconKey size="small" />}
|
||||
icon={
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<IconKey strokeWidth={2} size={16} />
|
||||
</div>
|
||||
}
|
||||
title="Auth"
|
||||
href={`/project/${ref}/auth/users`}
|
||||
/>
|
||||
@@ -156,7 +164,6 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
customDateFormat={datetimeFormat}
|
||||
data={charts}
|
||||
isLoading={!charts && !error ? true : false}
|
||||
onBarClick={(v) => handleBarClick(v, '/auth')}
|
||||
/>
|
||||
<ProjectUsageMinimal
|
||||
projectRef={project.ref}
|
||||
@@ -165,10 +172,16 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 lg:gap-8">
|
||||
<Panel key="storage-chart">
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader
|
||||
icon={<IconArchive size="small" />}
|
||||
icon={
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<IconArchive strokeWidth={2} size={16} />
|
||||
</div>
|
||||
}
|
||||
title="Storage"
|
||||
href={`/project/${ref}/storage/buckets`}
|
||||
/>
|
||||
@@ -183,7 +196,6 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
customDateFormat={datetimeFormat}
|
||||
data={charts}
|
||||
isLoading={!charts && !error ? true : false}
|
||||
onBarClick={(v) => handleBarClick(v, '/storage')}
|
||||
/>
|
||||
<ProjectUsageMinimal
|
||||
projectRef={project.ref}
|
||||
@@ -194,7 +206,14 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
</Panel>
|
||||
<Panel key="realtime-chart">
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader icon={<IconZap size="small" />} title="Realtime" />
|
||||
<PanelHeader
|
||||
icon={
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<IconZap strokeWidth={2} size={16} />
|
||||
</div>
|
||||
}
|
||||
title="Realtime"
|
||||
/>
|
||||
<ChartHandler
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
@@ -210,88 +229,76 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
isLoading={!charts && !error ? true : false}
|
||||
onBarClick={(v) => handleBarClick(v, '/realtime')}
|
||||
/>
|
||||
{/* Empty space just so the cards are of the same height */}
|
||||
<div className="py-[26px]" />
|
||||
{/* <ProjectUsageMinimal
|
||||
projectRef={project.ref}
|
||||
subscription_id={project.subscription_id}
|
||||
filter="Connection requests"
|
||||
/> */}
|
||||
<div className="py-[1.64rem]" />
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
{logsUsageCodesPaths && (
|
||||
<div className="grid lg:grid-cols-4 lg:gap-8">
|
||||
<Panel
|
||||
key="api-status-codes"
|
||||
className="col-start-1 col-span-2"
|
||||
wrapWithLoading={false}
|
||||
>
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader title="API Status Codes" />
|
||||
<StackedAreaChart
|
||||
dateFormat={datetimeFormat}
|
||||
data={codesData?.result}
|
||||
stackKey="status_code"
|
||||
xAxisKey="timestamp"
|
||||
yAxisKey="count"
|
||||
isLoading={!codesData && !codesFetchError ? true : false}
|
||||
xAxisFormatAsDate
|
||||
size="large"
|
||||
styleMap={{
|
||||
200: { stroke: USAGE_COLORS['200'], fill: USAGE_COLORS['200'] },
|
||||
201: { stroke: USAGE_COLORS['201'], fill: USAGE_COLORS['201'] },
|
||||
400: { stroke: USAGE_COLORS['400'], fill: USAGE_COLORS['400'] },
|
||||
401: { stroke: USAGE_COLORS['401'], fill: USAGE_COLORS['401'] },
|
||||
404: { stroke: USAGE_COLORS['404'], fill: USAGE_COLORS['404'] },
|
||||
500: { stroke: USAGE_COLORS['500'], fill: USAGE_COLORS['500'] },
|
||||
}}
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
<Panel
|
||||
key="top-routes"
|
||||
className="col-start-3 col-span-2 pb-0"
|
||||
bodyClassName="h-full"
|
||||
wrapWithLoading={false}
|
||||
>
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader title="Top Routes" />
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th>Path</Table.th>
|
||||
<Table.th>Count</Table.th>
|
||||
<Table.th>Avg. Latency (ms)</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{(pathsData?.result ?? []).map((row: PathsDatum) => (
|
||||
<Table.tr>
|
||||
<Table.td className="flex items-center">
|
||||
<div className="w-[60px]">
|
||||
<Typography.Text code small>
|
||||
{row.method}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className="font-mono" small>
|
||||
{row.path}
|
||||
</Typography.Text>
|
||||
</Table.td>
|
||||
<Table.td>{row.count}</Table.td>
|
||||
<Table.td>{Number(row.avg_origin_time).toFixed(2)}</Table.td>
|
||||
</Table.tr>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{logsUsageCodesPaths && (
|
||||
<div className="grid lg:grid-cols-4 lg:gap-8">
|
||||
<Panel
|
||||
key="api-status-codes"
|
||||
className="col-start-1 col-span-2"
|
||||
wrapWithLoading={false}
|
||||
>
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader title="API Status Codes" />
|
||||
<StackedAreaChart
|
||||
dateFormat={datetimeFormat}
|
||||
data={codesData?.result}
|
||||
stackKey="status_code"
|
||||
xAxisKey="timestamp"
|
||||
yAxisKey="count"
|
||||
isLoading={!codesData && !codesFetchError ? true : false}
|
||||
xAxisFormatAsDate
|
||||
size="large"
|
||||
styleMap={{
|
||||
200: { stroke: USAGE_COLORS['200'], fill: USAGE_COLORS['200'] },
|
||||
201: { stroke: USAGE_COLORS['201'], fill: USAGE_COLORS['201'] },
|
||||
400: { stroke: USAGE_COLORS['400'], fill: USAGE_COLORS['400'] },
|
||||
401: { stroke: USAGE_COLORS['401'], fill: USAGE_COLORS['401'] },
|
||||
404: { stroke: USAGE_COLORS['404'], fill: USAGE_COLORS['404'] },
|
||||
500: { stroke: USAGE_COLORS['500'], fill: USAGE_COLORS['500'] },
|
||||
}}
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
<Panel
|
||||
key="top-routes"
|
||||
className="col-start-3 col-span-2 pb-0"
|
||||
bodyClassName="h-full"
|
||||
wrapWithLoading={false}
|
||||
>
|
||||
<Panel.Content className="space-y-4">
|
||||
<PanelHeader title="Top Routes" />
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th>Path</Table.th>
|
||||
<Table.th>Count</Table.th>
|
||||
<Table.th>Avg. Latency (ms)</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{(pathsData?.result ?? []).map((row: PathsDatum) => (
|
||||
<Table.tr>
|
||||
<Table.td className="flex items-center space-x-2">
|
||||
<p className="font-mono text-sm text-scale-1200">{row.method}</p>
|
||||
<p className="font-mono text-sm">{row.path}</p>
|
||||
</Table.td>
|
||||
<Table.td>{row.count}</Table.td>
|
||||
<Table.td>{Number(row.avg_origin_time).toFixed(2)}</Table.td>
|
||||
</Table.tr>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -304,15 +311,13 @@ const PanelHeader = (props: any) => {
|
||||
<Tag href={props.href}>
|
||||
<div
|
||||
className={
|
||||
'flex items-center space-x-3 opacity-80 ' +
|
||||
(props.href ? 'cursor-pointer hover:opacity-100 hover:text-green-500' : '')
|
||||
'flex items-center space-x-3 opacity-80 transition ' +
|
||||
(props.href ? 'cursor-pointer hover:opacity-100 hover:text-gray-1200' : '')
|
||||
}
|
||||
>
|
||||
<Typography.Text>{props.icon}</Typography.Text>
|
||||
<span className="flex items-center space-x-1">
|
||||
<Typography.Title level={4} className="mb-0">
|
||||
{props.title}
|
||||
</Typography.Title>
|
||||
<h4 className="mb-0 text-lg">{props.title}</h4>
|
||||
</span>
|
||||
</div>
|
||||
</Tag>
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { FC } from 'react'
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
organization: any
|
||||
}
|
||||
|
||||
const AWSMarketplaceSubscription: FC<Props> = ({ organization }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Typography.Text>
|
||||
This organization is subscribed via AWS Marketplace, with a limit of{' '}
|
||||
{organization.project_limit} projects and expiring on{' '}
|
||||
{organization.aws_marketplace.Entitlements[0].ExpirationDate.slice(0, 10)}.
|
||||
</Typography.Text>
|
||||
|
||||
<Button>
|
||||
<a
|
||||
href="https://aws.amazon.com/marketplace/saas/ordering?productId=dc498450-cecf-44c2-8c99-c3c13f16e70a&offerId=d1htt16t3ygx5brtwhooms0ei"
|
||||
target="_blank"
|
||||
>
|
||||
Manage AWS Marketplace subscription
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AWSMarketplaceSubscription
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FC } from 'react'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { Input, Button } from '@supabase/ui'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
address: Dictionary<any>
|
||||
redirectToPortal: (url: string) => void
|
||||
}
|
||||
|
||||
const BillingAddress: FC<Props> = ({ loading, address, redirectToPortal }) => {
|
||||
const { city, country, line1, line2, postal_code, state } = address
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4>Billing address</h4>
|
||||
<p className="text-sm opacity-50">This will be reflected in every invoice</p>
|
||||
</div>
|
||||
<Panel
|
||||
loading={loading}
|
||||
footer={
|
||||
<div>
|
||||
<Button type="default" onClick={() => redirectToPortal('/customer/update')}>
|
||||
Edit billing address
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="w-3/5 space-y-2">
|
||||
<Input readOnly placeholder="Address line 1" value={line1 || ''} />
|
||||
<Input readOnly placeholder="Address line 2" value={line2 || ''} />
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input className="w-full" placeholder="Country" readOnly value={country || ''} />
|
||||
<Input
|
||||
className="w-full"
|
||||
placeholder="Postal code"
|
||||
readOnly
|
||||
value={postal_code || ''}
|
||||
/>
|
||||
</div>
|
||||
{city && <Input readOnly placeholder="City" value={city} />}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BillingAddress
|
||||
@@ -1,24 +1,17 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import {
|
||||
Typography,
|
||||
Badge,
|
||||
Button,
|
||||
IconTrash,
|
||||
IconCreditCard,
|
||||
Input,
|
||||
IconPlus,
|
||||
IconEdit2,
|
||||
IconLoader,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { getURL } from 'lib/helpers'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import ProjectSubscription from './ProjectSubscription'
|
||||
|
||||
import AWSMarketplaceSubscription from './AWSMarketplaceSubscription'
|
||||
import ProjectsSummary from './ProjectsSummary'
|
||||
import CreditBalance from './CreditBalance'
|
||||
import PaymentMethods from './PaymentMethods'
|
||||
import BillingAddress from './BillingAddress'
|
||||
import TaxID from './TaxID'
|
||||
|
||||
interface Props {
|
||||
organization: any
|
||||
@@ -35,7 +28,7 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
const [taxIds, setTaxIds] = useState<any>(null)
|
||||
const [paymentMethods, setPaymentMethods] = useState<any>(null)
|
||||
|
||||
const { stripe_customer_id, stripe_customer_object, name: orgName, slug: orgSlug } = organization
|
||||
const { stripe_customer_id, stripe_customer_object } = organization
|
||||
|
||||
const customerBalance = customer && customer.balance ? customer.balance / 100 : 0
|
||||
const isCredit = customerBalance < 0
|
||||
@@ -45,8 +38,6 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
? customerBalance.toString().replace('-', '')
|
||||
: customerBalance
|
||||
|
||||
const { city, country, line1, line2, postal_code, state } = stripe_customer_object?.address ?? {}
|
||||
|
||||
useEffect(() => {
|
||||
if (stripe_customer_id) {
|
||||
getStripeAccount()
|
||||
@@ -109,198 +100,27 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
return (
|
||||
<article className="my-4 container max-w-4xl space-y-8">
|
||||
{organization.aws_marketplace ? (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Typography.Text>
|
||||
This organization is subscribed via AWS Marketplace, with a limit of{' '}
|
||||
{organization.project_limit} projects and expiring on{' '}
|
||||
{organization.aws_marketplace.Entitlements[0].ExpirationDate.slice(0, 10)}.
|
||||
</Typography.Text>
|
||||
|
||||
<Button>
|
||||
<a
|
||||
href="https://aws.amazon.com/marketplace/saas/ordering?productId=dc498450-cecf-44c2-8c99-c3c13f16e70a&offerId=d1htt16t3ygx5brtwhooms0ei"
|
||||
target="_blank"
|
||||
>
|
||||
Manage AWS Marketplace subscription
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<AWSMarketplaceSubscription organization={organization} />
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 lg:col-span-7">
|
||||
<Panel
|
||||
loading={loading}
|
||||
title={
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Typography.Title level={5}>Billing Information</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
These will be reflected in all invoices
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button
|
||||
key="panel-footer"
|
||||
type="outline"
|
||||
icon={<IconEdit2 />}
|
||||
onClick={() => redirectToPortal('/customer/update')}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="space-y-2">
|
||||
<Input readOnly layout="horizontal" label="Country" value={country || ''} />
|
||||
{city && <Input readOnly layout="horizontal" label="City" value={city} />}
|
||||
<Input readOnly layout="horizontal" label="Address" value={line1 || ''} />
|
||||
<Input readOnly layout="horizontal" label="" value={line2 || ''} />
|
||||
<Input readOnly layout="horizontal" label="" value={postal_code || ''} />
|
||||
</Panel.Content>
|
||||
<Panel.Content className="space-y-2">
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-4">
|
||||
<Typography.Text>Tax IDs</Typography.Text>
|
||||
</div>
|
||||
<div className="col-span-8">
|
||||
{!taxIds ? (
|
||||
<div className="flex space-x-2 items-center">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<Typography.Text>Retrieving tax information...</Typography.Text>
|
||||
</div>
|
||||
) : (taxIds?.data ?? []).length === 0 ? (
|
||||
<Typography.Text type="secondary">None</Typography.Text>
|
||||
) : (
|
||||
<>
|
||||
{taxIds.data.map((taxId: any) => (
|
||||
<Input readOnly value={taxId.value} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-5">
|
||||
<Panel
|
||||
className="!mb-4"
|
||||
loading={loading}
|
||||
title={<Typography.Title level={5}>Balance</Typography.Title>}
|
||||
>
|
||||
<Panel.Content>
|
||||
{isCredit && <Badge>You have credits available</Badge>}
|
||||
{isDebt && <Badge color="red">Outstanding payments</Badge>}
|
||||
|
||||
<Typography.Title level={2} className="mb-0">
|
||||
<Typography.Text type="secondary">
|
||||
<span className="text-lg font-normal">$</span>
|
||||
</Typography.Text>
|
||||
|
||||
<span>{balance}</span>
|
||||
<Typography.Text type="secondary">
|
||||
{isCredit && <span className="text-lg font-normal"> / credits</span>}
|
||||
</Typography.Text>
|
||||
</Typography.Title>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
<Panel
|
||||
loading={loading}
|
||||
title={
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<Typography.Title level={5}>Payment methods</Typography.Title>
|
||||
<Button
|
||||
key="panel-footer"
|
||||
type="outline"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => redirectToPortal('/payment-methods')}
|
||||
>
|
||||
{(paymentMethods?.data ?? []).length >= 1 ? 'Add another' : 'Add new'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
{paymentMethods && paymentMethods.data.length >= 1 ? (
|
||||
<div className="space-y-1">
|
||||
{paymentMethods.data.map((paymentMethod: any) => {
|
||||
return (
|
||||
<div
|
||||
key={paymentMethod.id}
|
||||
className="rounded border border-border-secondary-light dark:border-border-secondary-dark p-3 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<img
|
||||
src={`/img/payment-methods/${paymentMethod.card.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
width="32"
|
||||
/>
|
||||
<Typography.Text>
|
||||
<span className="capitalize">{paymentMethod.card.brand}</span>{' '}
|
||||
ending in{' '}
|
||||
<span className="dark:text-white">{paymentMethod.card.last4}</span>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text>
|
||||
<span className="dark:text-white">
|
||||
{paymentMethod.card.exp_month}
|
||||
</span>
|
||||
/
|
||||
<span className="dark:text-white">
|
||||
{paymentMethod.card.exp_year}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => redirectToPortal('/')}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 flex items-center rounded border border-border-secondary-light dark:border-border-secondary-dark border-dashed">
|
||||
<div className="flex items-center w-full justify-between space-x-2 hover:text-white hover:border-white">
|
||||
<Typography.Text className="flex items-center space-x-2">
|
||||
<div>
|
||||
<IconCreditCard />
|
||||
</div>
|
||||
<div>No payment methods</div>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Panel.Content>
|
||||
<div className="flex items-center w-full justify-between space-x-2 hover:text-white hover:border-white"></div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{projects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center px-16 py-12 border border-border-secondary-light dark:border-border-secondary-dark border-dashed rounded space-y-3">
|
||||
<Typography.Text className="block" type="secondary">
|
||||
You have no projects in organization {orgName} yet
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Link href={`/new/${orgSlug}`}>
|
||||
<Button type="outline">Create a project</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Text className="block mb-4">Projects in organization</Typography.Text>
|
||||
)}
|
||||
<div className="space-y-8">
|
||||
{projects.map((project) => {
|
||||
return <ProjectSubscription key={project.id} project={project} />
|
||||
})}
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<ProjectsSummary projects={projects} />
|
||||
<CreditBalance balance={balance} isCredit={isCredit} isDebt={isDebt} />
|
||||
<PaymentMethods
|
||||
loading={loading}
|
||||
paymentMethods={paymentMethods?.data ?? []}
|
||||
redirectToPortal={redirectToPortal}
|
||||
/>
|
||||
<BillingAddress
|
||||
loading={loading}
|
||||
address={stripe_customer_object?.address ?? {}}
|
||||
redirectToPortal={redirectToPortal}
|
||||
/>
|
||||
<TaxID
|
||||
loading={loading}
|
||||
taxIds={taxIds?.data ?? []}
|
||||
redirectToPortal={redirectToPortal}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { FC } from 'react'
|
||||
import { Badge } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
balance: string | number
|
||||
isCredit: boolean
|
||||
isDebt: boolean
|
||||
}
|
||||
|
||||
const CreditBalance: FC<Props> = ({ balance, isCredit, isDebt }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4>Credit balance</h4>
|
||||
{isCredit && <Badge>You have credits available</Badge>}
|
||||
{isDebt && <Badge color="red">Outstanding payments</Badge>}
|
||||
</div>
|
||||
<p className="text-sm opacity-50">Charges will be deducted from your balance first</p>
|
||||
</div>
|
||||
<div className="flex items-end space-x-1">
|
||||
{isDebt && <h4 className="opacity-50">-</h4>}
|
||||
<h4 className="opacity-50">$</h4>
|
||||
<h2 className="text-4xl">{balance}</h2>
|
||||
{isCredit && <h4 className="opacity-50">/credits</h4>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreditBalance
|
||||
@@ -0,0 +1,81 @@
|
||||
import { FC } from 'react'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import { IconPlus, IconEdit2, IconCreditCard, Button, Input, IconLoader } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
paymentMethods: any[]
|
||||
redirectToPortal: (url: string) => void
|
||||
}
|
||||
|
||||
const PaymentMethods: FC<Props> = ({ loading, paymentMethods, redirectToPortal }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4>Payment methods</h4>
|
||||
<p className="text-sm opacity-50">Charges will be deducted from the default card</p>
|
||||
</div>
|
||||
<Panel
|
||||
loading={loading}
|
||||
footer={
|
||||
<div>
|
||||
<Button
|
||||
key="panel-footer"
|
||||
type="default"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => redirectToPortal('/payment-methods')}
|
||||
>
|
||||
Add new card
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
{loading && paymentMethods.length === 0 ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<p className="text-sm">Retrieving payment methods</p>
|
||||
</div>
|
||||
) : paymentMethods.length >= 1 ? (
|
||||
<div className="space-y-2">
|
||||
{paymentMethods.map((paymentMethod: any) => (
|
||||
<div key={paymentMethod.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-8">
|
||||
<img
|
||||
src={`/img/payment-methods/${paymentMethod.card.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
width="32"
|
||||
/>
|
||||
<Input
|
||||
readOnly
|
||||
size="small"
|
||||
value={`•••• •••• •••• ${paymentMethod.card.last4}`}
|
||||
/>
|
||||
<p>
|
||||
{paymentMethod.card.exp_month}/{paymentMethod.card.exp_year}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="outline" icon={<IconEdit2 />} onClick={() => redirectToPortal('/')}>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="flex items-center space-x-2 opacity-50">
|
||||
<div>
|
||||
<IconCreditCard />
|
||||
</div>
|
||||
<div>No payment methods</div>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaymentMethods
|
||||
@@ -1,95 +0,0 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { get as _get, maxBy } from 'lodash'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { useStore, useSubscriptionStats } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { get, post } from 'lib/common/fetch'
|
||||
import { DATE_FORMAT, STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { Subscription, StripeSubscription, chargeableProducts } from 'components/interfaces/Billing'
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
interface Props {
|
||||
project: any
|
||||
}
|
||||
|
||||
const ProjectSubscription: FC<Props> = ({ project }) => {
|
||||
const { ui } = useStore()
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [subscription, setSubscription] = useState<StripeSubscription>()
|
||||
const [paygStats, setPaygStats] = useState<Dictionary<number>>()
|
||||
|
||||
useEffect(() => {
|
||||
getSubscription()
|
||||
}, [])
|
||||
|
||||
const getSubscription = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { data: subscription, error }: { data: StripeSubscription; error: any } = await post(
|
||||
`${API_URL}/stripe/subscription`,
|
||||
{
|
||||
subscription_id: project.subscription_id,
|
||||
}
|
||||
)
|
||||
if (error) throw error
|
||||
setSubscription(subscription)
|
||||
|
||||
if (subscription.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG) {
|
||||
fetchPaygStatistics()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to get project subscription: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPaygStatistics = async () => {
|
||||
const startDate = dayjs().utc().startOf('month').format(DATE_FORMAT)
|
||||
const endDate = dayjs().utc().endOf('month').format(DATE_FORMAT)
|
||||
const attributes =
|
||||
'total_db_size_bytes,total_db_egress_bytes,total_storage_size_bytes,total_storage_egress'
|
||||
const url = `${API_URL}/projects/${
|
||||
project.ref
|
||||
}/daily-stats?attribute=${attributes}&startDate=${encodeURIComponent(
|
||||
startDate
|
||||
)}&endDate=${encodeURIComponent(endDate)}&interval='1d'`
|
||||
|
||||
const { data } = await get(url)
|
||||
|
||||
const paygStats: any = {}
|
||||
chargeableProducts.forEach((product: any) => {
|
||||
product.features.forEach((feature: any) => {
|
||||
paygStats[feature.attribute] = _get(maxBy(data, feature.attribute), feature.attribute)
|
||||
})
|
||||
})
|
||||
setPaygStats(paygStats)
|
||||
}
|
||||
|
||||
return (
|
||||
<Subscription
|
||||
showProjectName
|
||||
loading={loading}
|
||||
project={project}
|
||||
// @ts-ignore
|
||||
subscription={subscription}
|
||||
paygStats={paygStats}
|
||||
subscriptionStats={subscriptionStats}
|
||||
// @ts-ignore
|
||||
currentPeriodStart={subscription?.billing.current_period_start}
|
||||
// @ts-ignore
|
||||
currentPeriodEnd={subscription?.billing.current_period_end}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectSubscription
|
||||
@@ -0,0 +1,116 @@
|
||||
import Link from 'next/link'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { IconChevronRight, IconLoader } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import { StripeSubscription } from 'components/interfaces/Billing'
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
interface ProjectSummaryProps {
|
||||
project: any
|
||||
}
|
||||
|
||||
const ProjectSummary: FC<ProjectSummaryProps> = ({ project }) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [subscription, setSubscription] = useState<StripeSubscription>()
|
||||
|
||||
const currentPeriodStart = subscription?.billing?.current_period_start ?? 0
|
||||
const currentPeriodEnd = subscription?.billing?.current_period_end ?? 0
|
||||
|
||||
useEffect(() => {
|
||||
getSubscription()
|
||||
}, [])
|
||||
|
||||
const getSubscription = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { data: subscription, error }: { data: StripeSubscription; error: any } = await post(
|
||||
`${API_URL}/stripe/subscription`,
|
||||
{ subscription_id: project.subscription_id }
|
||||
)
|
||||
if (error) throw error
|
||||
setSubscription(subscription)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to get project subscription: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 py-3 flex items-center">
|
||||
<div className="w-[25%]">
|
||||
<p className="text-sm">{project.name}</p>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-[75%] flex items-center justify-center">
|
||||
<IconLoader size={14} className="animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-[20%]">
|
||||
<p className="text-sm">{subscription?.tier.name ?? ''}</p>
|
||||
</div>
|
||||
<div className="w-[40%] space-x-2 flex items-center">
|
||||
<p className="text-sm">{dayjs.unix(currentPeriodStart).utc().format('MMM D, YYYY')}</p>
|
||||
<p className="text-sm">-</p>
|
||||
<p className="text-sm">{dayjs.unix(currentPeriodEnd).utc().format('MMM D, YYYY')}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end w-[15%]">
|
||||
<Link href={`/project/${project.ref}/settings/billing`}>
|
||||
<a className="flex items-center space-x-2 group">
|
||||
<p className="transition group-hover:opacity-100 opacity-0 text-sm">View details</p>
|
||||
<IconChevronRight />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProjectsSummaryProps {
|
||||
projects: any
|
||||
}
|
||||
|
||||
const ProjectsSummary: FC<ProjectsSummaryProps> = ({ projects }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4>Projects at a glance</h4>
|
||||
<Panel
|
||||
title={
|
||||
<div className="w-full flex items-center">
|
||||
<div className="w-[25%]">
|
||||
<p className="text-sm opacity-50">Name</p>
|
||||
</div>
|
||||
<div className="w-[20%]">
|
||||
<p className="text-sm opacity-50">Plan</p>
|
||||
</div>
|
||||
<div className="w-[40%]">
|
||||
<p className="text-sm opacity-50">Billing cycle</p>
|
||||
</div>
|
||||
<div className="w-[15%]" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{projects.map((project: any) => (
|
||||
<ProjectSummary key={project.ref} project={project} />
|
||||
))}
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectsSummary
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FC } from 'react'
|
||||
import { Input, Button, IconLoader } from '@supabase/ui'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
taxIds: any[]
|
||||
redirectToPortal: (url: string) => void
|
||||
}
|
||||
|
||||
const TaxID: FC<Props> = ({ loading, taxIds, redirectToPortal }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4>Tax ID</h4>
|
||||
<p className="text-sm opacity-50">
|
||||
If you would like to include specific tax ID(s) to your invoices
|
||||
</p>
|
||||
</div>
|
||||
<Panel
|
||||
loading={loading}
|
||||
footer={
|
||||
<div>
|
||||
<Button type="default" onClick={() => redirectToPortal('/customer/update')}>
|
||||
Edit Tax IDs
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="w-3/5">
|
||||
{loading && taxIds.length === 0 ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<p className="text-sm">Retrieving tax information</p>
|
||||
</div>
|
||||
) : taxIds.length >= 1 ? (
|
||||
<div className="w-full space-y-2">
|
||||
{taxIds.map((taxId: any) => {
|
||||
const taxIdType = taxId.type.replace('_', ' ').toUpperCase()
|
||||
return (
|
||||
<div key={taxId.id} className="flex items-center space-x-6">
|
||||
<p>{taxIdType}</p>
|
||||
<Input readOnly value={taxId.value} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="flex items-center space-x-2 text-sm text-scale-900">No tax IDs</p>
|
||||
</div>
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaxID
|
||||
@@ -130,20 +130,20 @@ const InvoicesSettings: FC<Props> = ({ organization }) => {
|
||||
<Table.tr key="navigation">
|
||||
<Table.td colSpan={5}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text type="secondary" small>
|
||||
<p className="text-sm opacity-50">
|
||||
Showing {offset + 1} to {offset + invoices.length} out of {count} invoices
|
||||
</Typography.Text>
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
icon={<IconChevronLeft />}
|
||||
type="secondary"
|
||||
type="default"
|
||||
size="tiny"
|
||||
disabled={page === 1}
|
||||
onClick={async () => await fetchInvoices(page - 1)}
|
||||
/>
|
||||
<Button
|
||||
icon={<IconChevronRight />}
|
||||
type="secondary"
|
||||
type="default"
|
||||
size="tiny"
|
||||
disabled={page * PAGE_LIMIT >= count}
|
||||
onClick={async () => await fetchInvoices(page + 1)}
|
||||
|
||||
@@ -29,8 +29,8 @@ const ResultsPane: FC<Props> = ({ results }) => {
|
||||
}
|
||||
const columnRender = (name: string) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center font-mono h-full">
|
||||
<Typography.Text small>{name}</Typography.Text>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-sm font-mono">{name}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,14 +96,16 @@ const LogPanel: FC<Props> = ({
|
||||
<div
|
||||
className={[
|
||||
'absolute flex items-center justify-center -top-3 right-3',
|
||||
'h-3 w-3 z-50',
|
||||
'h-4 w-4 z-50',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="absolute z-20">
|
||||
<Typography.Text style={{ fontSize: '0.7rem' }} className="opacity-80">{newCount}</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: '0.6rem' }} className="opacity-80">
|
||||
{newCount}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="bg-green-600 rounded-full w-full h-full animate-ping opacity-40"></div>
|
||||
<div className="absolute z-60 top-0 right-0 bg-green-900 opacity-60 rounded-full w-full h-full "></div>
|
||||
<div className="bg-green-800 rounded-full w-full h-full animate-ping opacity-60"></div>
|
||||
<div className="absolute z-60 top-0 right-0 bg-green-900 opacity-80 rounded-full w-full h-full"></div>
|
||||
</div>
|
||||
)}
|
||||
<IconRefreshCw />
|
||||
@@ -129,9 +131,7 @@ const LogPanel: FC<Props> = ({
|
||||
</Dropdown>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text type="secondary" small>
|
||||
Search logs via query
|
||||
</Typography.Text>
|
||||
<p className="text-xs">Search logs via query</p>
|
||||
<Toggle size="tiny" checked={isCustomQuery} onChange={onCustomClick} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,9 +188,7 @@ const LogPanel: FC<Props> = ({
|
||||
</div>
|
||||
{!isCustomQuery && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text type="secondary" small>
|
||||
Show event chart
|
||||
</Typography.Text>
|
||||
<p className="text-xs">Show event chart</p>
|
||||
<Toggle size="tiny" checked={isShowingEventChart} onChange={onToggleEventChart} />
|
||||
</div>
|
||||
)}
|
||||
@@ -218,7 +216,7 @@ const LogPanel: FC<Props> = ({
|
||||
/>
|
||||
),
|
||||
|
||||
<Button key="go" size="tiny" title="Go" type="secondary" onClick={handleSearch}>
|
||||
<Button key="go" size="tiny" title="Go" type="default" onClick={handleSearch}>
|
||||
<IconSearch size={16} />
|
||||
</Button>,
|
||||
]}
|
||||
|
||||
@@ -45,7 +45,7 @@ const LogTable = ({ isCustomQuery, data = [] }: Props) => {
|
||||
className={[
|
||||
'block whitespace-wrap font-mono',
|
||||
`${hasLogDataFormat && row.id === focusedLog?.id ? 'font-bold' : ''}`,
|
||||
`${hasLogDataFormat && v === 'timestamp' ? 'text-green-500' : ''}`,
|
||||
`${hasLogDataFormat && v === 'timestamp' ? 'text-green-900' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{value}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
import { Button } from '@supabase/ui'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
@@ -15,7 +15,7 @@ const EmptyState: FC<Props> = ({ selectedSchema, onAddTable }) => {
|
||||
const renderNoTablesCTA = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<Typography.Text>There are no tables available in this schema</Typography.Text>
|
||||
<p className="text-sm">There are no tables available in this schema</p>
|
||||
{selectedSchema === 'public' && <Button onClick={onAddTable}>Create a new table</Button>}
|
||||
</div>
|
||||
)
|
||||
@@ -27,7 +27,7 @@ const EmptyState: FC<Props> = ({ selectedSchema, onAddTable }) => {
|
||||
renderNoTablesCTA()
|
||||
) : (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Typography.Text>Select a table or create a new one</Typography.Text>
|
||||
<p className="text-sm">Select a table or create a new one</p>
|
||||
<Button onClick={onAddTable}>Create a new table</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Button, IconAlertCircle, Typography } from '@supabase/ui'
|
||||
import { Badge, Button, IconAlertCircle, Typography } from '@supabase/ui'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
@@ -18,20 +18,9 @@ const GridHeaderActions: FC<Props> = ({ table }) => {
|
||||
<div className="space-x-3 flex items-center">
|
||||
{!table.rls_enabled && (
|
||||
<Link href={urlToRLSPolicies}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className="
|
||||
px-2 py-1
|
||||
border border-yellow-400 border-opacity-10
|
||||
bg-yellow-400 bg-opacity-10 rounded-md flex items-center
|
||||
text-yellow-400 space-x-2 hover:bg-opacity-20
|
||||
transition-opacity
|
||||
cursor-pointer"
|
||||
>
|
||||
<IconAlertCircle strokeWidth={2} size={16} />
|
||||
<span className="text-xs">RLS not enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="warning" icon={<IconAlertCircle strokeWidth={2} size={14} />}>
|
||||
RLS not enabled
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{/* <Button type="text">
|
||||
|
||||
@@ -29,17 +29,17 @@ const ActionBar: FC<ActionBarProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-x-3 w-full flex justify-end px-3">
|
||||
<Button onClick={closePanel} type="text" size="medium">
|
||||
<div className="space-x-3 w-full flex justify-between px-3 py-4 border-t border-scale-500">
|
||||
<Button size="small" onClick={closePanel} type="default">
|
||||
{backButtonLabel}
|
||||
</Button>
|
||||
{children}
|
||||
{applyFunction && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={onSelectApply}
|
||||
disabled={disableApply || isRunning}
|
||||
loading={isRunning}
|
||||
size="medium"
|
||||
>
|
||||
{applyButtonLabel}
|
||||
</Button>
|
||||
|
||||
+123
-108
@@ -128,13 +128,14 @@ const ColumnEditor: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
size="large"
|
||||
key="ColumnEditor"
|
||||
visible={visible}
|
||||
// @ts-ignore
|
||||
title={<HeaderTitle table={selectedTable} column={column} />}
|
||||
onCancel={closePanel}
|
||||
onConfirm={(resolve: () => void) => onSaveChanges(resolve)}
|
||||
// @ts-ignore
|
||||
header={<HeaderTitle table={selectedTable} column={column} />}
|
||||
onCancel={closePanel}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Cancel"
|
||||
@@ -144,117 +145,131 @@ const ColumnEditor: FC<Props> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Input
|
||||
label="Name"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
error={errors.name}
|
||||
value={columnFields?.name ?? ''}
|
||||
onChange={(event: any) => onUpdateField({ name: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Description"
|
||||
placeholder="Optional"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
value={columnFields?.comment ?? ''}
|
||||
onChange={(event: any) => onUpdateField({ comment: event.target.value })}
|
||||
/>
|
||||
{isNewRecord && !hasPrimaryKey && (
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label={hasPrimaryKey ? 'Add to composite primary key' : 'Is Primary Key'}
|
||||
description="A primary key indicates that a column or group of columns can be used as a unique identifier for rows in the table."
|
||||
checked={columnFields?.isPrimaryKey ?? false}
|
||||
onChange={() => onUpdateField({ isPrimaryKey: !columnFields?.isPrimaryKey })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ColumnForeignKey
|
||||
column={columnFields}
|
||||
originalForeignKey={foreignKey}
|
||||
onSelectEditRelation={() => setIsEditingRelation(true)}
|
||||
onSelectRemoveRelation={() => onUpdateField({ foreignKey: undefined })}
|
||||
/>
|
||||
<Divider />
|
||||
<ColumnType
|
||||
value={columnFields?.format ?? ''}
|
||||
enumTypes={enumTypes}
|
||||
error={errors.format}
|
||||
disabled={!isUndefined(columnFields?.foreignKey)}
|
||||
onOptionSelect={(format: string) => onUpdateField({ format, defaultValue: '' })}
|
||||
/>
|
||||
{isUndefined(columnFields.foreignKey) && (
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
{columnFields.format.includes('int') && (
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Is Identity"
|
||||
description="Automatically assign a sequential unique number to the column"
|
||||
checked={columnFields.isIdentity}
|
||||
onChange={() => {
|
||||
const isIdentity = !columnFields.isIdentity
|
||||
const isArray = isIdentity ? false : columnFields.isArray
|
||||
onUpdateField({ isIdentity, isArray })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!columnFields.isPrimaryKey && (
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Define as Array"
|
||||
description="Allow column to be defined as variable-length multidimensional arrays"
|
||||
checked={columnFields.isArray}
|
||||
onChange={() => {
|
||||
const isArray = !columnFields.isArray
|
||||
const isIdentity = isArray ? false : columnFields.isIdentity
|
||||
onUpdateField({ isArray, isIdentity })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ColumnDefaultValue
|
||||
columnFields={columnFields}
|
||||
enumTypes={enumTypes}
|
||||
onUpdateField={onUpdateField}
|
||||
/>
|
||||
{!columnFields.isPrimaryKey && (
|
||||
<>
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
<Input
|
||||
label="Name"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
error={errors.name}
|
||||
value={columnFields?.name ?? ''}
|
||||
onChange={(event: any) => onUpdateField({ name: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Description"
|
||||
placeholder="Optional"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
value={columnFields?.comment ?? ''}
|
||||
onChange={(event: any) => onUpdateField({ comment: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
|
||||
<SidePanel.Seperator />
|
||||
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
{isNewRecord && !hasPrimaryKey && (
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Allow Nullable"
|
||||
description="Allow the column to assume a NULL value if no value is provided"
|
||||
checked={columnFields.isNullable}
|
||||
onChange={() => onUpdateField({ isNullable: !columnFields.isNullable })}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Is Unique"
|
||||
description="Enforce values in the column to be unique across rows"
|
||||
checked={columnFields.isUnique}
|
||||
onChange={() => onUpdateField({ isUnique: !columnFields.isUnique })}
|
||||
label={hasPrimaryKey ? 'Add to composite primary key' : 'Is Primary Key'}
|
||||
description="A primary key indicates that a column or group of columns can be used as a unique identifier for rows in the table."
|
||||
checked={columnFields?.isPrimaryKey ?? false}
|
||||
onChange={() => onUpdateField({ isPrimaryKey: !columnFields?.isPrimaryKey })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
<ForeignKeySelector
|
||||
tables={tables}
|
||||
column={columnFields}
|
||||
foreignKey={foreignKey}
|
||||
visible={isEditingRelation}
|
||||
closePanel={() => setIsEditingRelation(false)}
|
||||
saveChanges={saveColumnForeignKey}
|
||||
/>
|
||||
)}
|
||||
<ColumnForeignKey
|
||||
column={columnFields}
|
||||
originalForeignKey={foreignKey}
|
||||
onSelectEditRelation={() => setIsEditingRelation(true)}
|
||||
onSelectRemoveRelation={() => onUpdateField({ foreignKey: undefined })}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
<ColumnType
|
||||
value={columnFields?.format ?? ''}
|
||||
enumTypes={enumTypes}
|
||||
error={errors.format}
|
||||
disabled={!isUndefined(columnFields?.foreignKey)}
|
||||
onOptionSelect={(format: string) => onUpdateField({ format, defaultValue: '' })}
|
||||
/>
|
||||
{isUndefined(columnFields.foreignKey) && (
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
{columnFields.format.includes('int') && (
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Is Identity"
|
||||
description="Automatically assign a sequential unique number to the column"
|
||||
checked={columnFields.isIdentity}
|
||||
onChange={() => {
|
||||
const isIdentity = !columnFields.isIdentity
|
||||
const isArray = isIdentity ? false : columnFields.isArray
|
||||
onUpdateField({ isIdentity, isArray })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!columnFields.isPrimaryKey && (
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Define as Array"
|
||||
description="Allow column to be defined as variable-length multidimensional arrays"
|
||||
checked={columnFields.isArray}
|
||||
onChange={() => {
|
||||
const isArray = !columnFields.isArray
|
||||
const isIdentity = isArray ? false : columnFields.isIdentity
|
||||
onUpdateField({ isArray, isIdentity })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ColumnDefaultValue
|
||||
columnFields={columnFields}
|
||||
enumTypes={enumTypes}
|
||||
onUpdateField={onUpdateField}
|
||||
/>
|
||||
{!columnFields.isPrimaryKey && (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Allow Nullable"
|
||||
description="Allow the column to assume a NULL value if no value is provided"
|
||||
checked={columnFields.isNullable}
|
||||
onChange={() => onUpdateField({ isNullable: !columnFields.isNullable })}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Is Unique"
|
||||
description="Enforce values in the column to be unique across rows"
|
||||
checked={columnFields.isUnique}
|
||||
onChange={() => onUpdateField({ isUnique: !columnFields.isUnique })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ForeignKeySelector
|
||||
tables={tables}
|
||||
column={columnFields}
|
||||
foreignKey={foreignKey}
|
||||
visible={isEditingRelation}
|
||||
closePanel={() => setIsEditingRelation(false)}
|
||||
saveChanges={saveColumnForeignKey}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
+17
-23
@@ -23,11 +23,9 @@ const ColumnForeignKey: FC<Props> = ({
|
||||
const hasNoForeignKey = isUndefined(originalForeignKey) && isUndefined(column?.foreignKey)
|
||||
if (hasNoForeignKey) {
|
||||
return (
|
||||
<div>
|
||||
<Button type="secondary" onClick={onSelectEditRelation}>
|
||||
Add foreign key relation
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="default" onClick={onSelectEditRelation}>
|
||||
Add foreign key relation
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,18 +88,16 @@ const ColumnForeignKeyInformation: FC<{
|
||||
block
|
||||
icon={<IconLink />}
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between text-scale-900">
|
||||
<div className="space-y-2">
|
||||
<Typography.Text>This column has the following foreign key relation:</Typography.Text>
|
||||
<span>This column has the following foreign key relation:</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text code small>
|
||||
{columnName}
|
||||
</Typography.Text>
|
||||
<span className="text-code">{columnName}</span>
|
||||
<IconArrowRight size={14} strokeWidth={2} />
|
||||
<Typography.Text code small>
|
||||
<span className="text-code">
|
||||
{foreignKey?.target_table_schema}.{foreignKey?.target_table_name}.
|
||||
{foreignKey?.target_column_name}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -129,21 +125,19 @@ const ColumnForeignKeyAdded: FC<{
|
||||
block
|
||||
icon={<IconLink />}
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between text-scale-1100">
|
||||
<div className="space-y-2">
|
||||
<Typography.Text>
|
||||
<span>
|
||||
The following foreign key relation will be{' '}
|
||||
<span className="text-green-500">added</span>:
|
||||
</Typography.Text>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text code small>
|
||||
{columnName}
|
||||
</Typography.Text>
|
||||
<span className="text-green-900">added</span>:
|
||||
</span>
|
||||
<div className="flex items-center space-x-2 text-scale-1200">
|
||||
<span className="text-code">{columnName}</span>
|
||||
<IconArrowRight size={14} strokeWidth={2} />
|
||||
<Typography.Text code small>
|
||||
<span className="text-code">
|
||||
{foreignKey?.target_table_schema}.{foreignKey?.target_table_name}.
|
||||
{foreignKey?.target_column_name}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -174,7 +168,7 @@ const ColumnForeignKeyRemoved: FC<{
|
||||
<div className="space-y-2">
|
||||
<Typography.Text>
|
||||
The following foreign key relation will be{' '}
|
||||
<span className="text-yellow-500">removed</span> from this column:
|
||||
<span className="text-amber-900">removed</span> from this column:
|
||||
</Typography.Text>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text code small>
|
||||
|
||||
+9
-14
@@ -34,19 +34,19 @@ const ColumnType: FC<Props> = ({
|
||||
const inferIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'number':
|
||||
return <IconHash size={16} className="text-green-600" strokeWidth={2} />
|
||||
return <IconHash size={16} className="text-scale-1200" strokeWidth={1.5} />
|
||||
case 'time':
|
||||
return <IconCalendar size={16} className="text-green-600" strokeWidth={2} />
|
||||
return <IconCalendar size={16} className="text-scale-1200" strokeWidth={1.5} />
|
||||
case 'text':
|
||||
return <IconType size={16} className="text-green-600" strokeWidth={2} />
|
||||
return <IconType size={16} className="text-scale-1200" strokeWidth={1.5} />
|
||||
case 'json':
|
||||
return (
|
||||
<div className="text-green-600" style={{ padding: '0px 1px' }}>
|
||||
<div className="text-scale-1200" style={{ padding: '0px 1px' }}>
|
||||
{'{ }'}
|
||||
</div>
|
||||
)
|
||||
case 'bool':
|
||||
return <IconToggleRight size={16} className="text-green-600" strokeWidth={2} />
|
||||
return <IconToggleRight size={16} className="text-scale-1200" strokeWidth={1.5} />
|
||||
default:
|
||||
return <div />
|
||||
}
|
||||
@@ -61,6 +61,7 @@ const ColumnType: FC<Props> = ({
|
||||
error={error}
|
||||
className={`${className} ${disabled ? 'column-type-disabled' : ''}`}
|
||||
onChange={(value: string) => onOptionSelect(value)}
|
||||
optionsWidth={480}
|
||||
>
|
||||
<Listbox.Option key="empty" value="" label="---">
|
||||
---
|
||||
@@ -77,7 +78,7 @@ const ColumnType: FC<Props> = ({
|
||||
value={enumType.name}
|
||||
label={enumType.name}
|
||||
addOnBefore={() => {
|
||||
return <div className="mx-1 w-2 h-2 rounded-full bg-green-700" />
|
||||
return <div className="mx-1 w-2 h-2 rounded-full bg-scale-1200" />
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
@@ -113,14 +114,8 @@ const ColumnType: FC<Props> = ({
|
||||
addOnBefore={() => inferIcon(option.type)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Typography.Text>
|
||||
<p>{option.name}</p>
|
||||
</Typography.Text>
|
||||
<p>
|
||||
<Typography.Text small className="opacity-50">
|
||||
{option.description}
|
||||
</Typography.Text>
|
||||
</p>
|
||||
<span className="text-scale-1200">{option.name}</span>
|
||||
<span className="text-scale-900">{option.description}</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
|
||||
+28
-53
@@ -6,9 +6,8 @@
|
||||
// the component over to the UI library
|
||||
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { Input, Typography } from '@supabase/ui'
|
||||
import { Button, Dropdown, IconList, Input } from '@supabase/ui'
|
||||
import { Suggestion } from './ColumnEditor.types'
|
||||
import { timeout } from 'lib/helpers'
|
||||
|
||||
const MAX_SUGGESTIONS = 3
|
||||
const DEFAULT_SUGGESTIONS_WIDTH = 400
|
||||
@@ -45,21 +44,8 @@ const InputWithSuggestions: FC<Props> = ({
|
||||
onSelectSuggestion = () => {},
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
const [isFocused, setIsFocused] = useState<boolean>(false)
|
||||
const [derviedSuggestionsWidth, setDerivedSuggestionsWidth] =
|
||||
useState<number>(DEFAULT_SUGGESTIONS_WIDTH)
|
||||
const [filteredSuggestions, setFilteredSuggestions] = useState<Suggestion[]>(suggestions)
|
||||
const showSuggestions = isFocused && filteredSuggestions.length > 0
|
||||
|
||||
useEffect(() => {
|
||||
// This is super hacky but will go away when we bring it into the UI library
|
||||
// It's mainly cause I can't pass a ref into Input
|
||||
const parentInputFieldWidth = (ref?.current as any).children[0]?.children[0]?.children[
|
||||
layout === 'horizontal' ? 1 : 0
|
||||
]?.offsetWidth
|
||||
const width = suggestionsWidth || parentInputFieldWidth || DEFAULT_SUGGESTIONS_WIDTH
|
||||
setDerivedSuggestionsWidth(width)
|
||||
}, [ref.current])
|
||||
const showSuggestions = filteredSuggestions.length > 0
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredSuggestions(suggestions.slice(0, MAX_SUGGESTIONS))
|
||||
@@ -92,45 +78,34 @@ const InputWithSuggestions: FC<Props> = ({
|
||||
className={className}
|
||||
type="text"
|
||||
value={value}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={async () => {
|
||||
await timeout(100)
|
||||
setIsFocused(false)
|
||||
}}
|
||||
onChange={onInputChange}
|
||||
actions={
|
||||
showSuggestions && (
|
||||
<Dropdown
|
||||
size={'medium'}
|
||||
align="end"
|
||||
side="bottom"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Label>Suggestions</Dropdown.Label>
|
||||
<Dropdown.Seperator />
|
||||
{filteredSuggestions.map((suggestion: Suggestion) => (
|
||||
<Dropdown.Item
|
||||
key={suggestion.name}
|
||||
onClick={() => onSelectSuggestion(suggestion)}
|
||||
>
|
||||
<div className="text-sm">{suggestion.name}</div>
|
||||
<div className="text-xs text-scale-900">{suggestion.description}</div>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button as="span" type="default" icon={<IconList strokeWidth={1.5} />}></Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{showSuggestions && (
|
||||
<div
|
||||
className="z-10 absolute top-11 right-0"
|
||||
style={{ width: `calc(${derviedSuggestionsWidth}px)` }}
|
||||
>
|
||||
<div className="bg-gray-800 border border-gray-600 shadow rounded-md py-1">
|
||||
<p className="px-4">
|
||||
<Typography.Text small className="opacity-50">
|
||||
{suggestionsHeader}
|
||||
</Typography.Text>
|
||||
</p>
|
||||
<div className="flex flex-col mt-2">
|
||||
{filteredSuggestions.map((suggestion: Suggestion) => (
|
||||
<div
|
||||
key={suggestion.name}
|
||||
className="px-4 py-2 cursor-pointer flex grid grid-cols-12 gap-2 hover:bg-green-600 !bg-opacity-40"
|
||||
onClick={() => onSelectSuggestion(suggestion)}
|
||||
>
|
||||
<div className="col-span-6">
|
||||
<Typography.Text>{suggestion.name}</Typography.Text>
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<Typography.Text small className="opacity-50">
|
||||
{suggestion.description}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+63
-67
@@ -105,15 +105,16 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
key="ColumnConfiguration"
|
||||
key="ForeignKeySelector"
|
||||
size="medium"
|
||||
visible={visible}
|
||||
onCancel={closePanel}
|
||||
// @ts-ignore
|
||||
title={
|
||||
<div>
|
||||
header={
|
||||
<span>
|
||||
Edit foreign key relation for{' '}
|
||||
<Typography.Text code>{get(column, ['name'], '')}</Typography.Text>
|
||||
</div>
|
||||
<span className="text-code">{get(column, ['name'], '')}</span>
|
||||
</span>
|
||||
}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
@@ -124,72 +125,67 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<InformationBox
|
||||
block
|
||||
icon={<IconHelpCircle size="large" strokeWidth={1.5} />}
|
||||
title="What are foreign keys?"
|
||||
description={
|
||||
<p>
|
||||
Foreign keys help maintain referential integrity of your data by ensuring that no one
|
||||
can insert rows into the table that do not have a matching entry to another table
|
||||
</p>
|
||||
}
|
||||
url="https://www.postgresql.org/docs/current/tutorial-fk.html"
|
||||
urlLabel="Postgres Foreign Key Documentation"
|
||||
/>
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-6">
|
||||
<InformationBox
|
||||
icon={<IconHelpCircle size="large" strokeWidth={1.5} />}
|
||||
title="What are foreign keys?"
|
||||
description={`Foreign keys help maintain referential integrity of your data by ensuring that no
|
||||
one can insert rows into the table that do not have a matching entry to another
|
||||
table.`}
|
||||
url="https://www.postgresql.org/docs/current/tutorial-fk.html"
|
||||
urlLabel="Postgres Foreign Key Documentation"
|
||||
/>
|
||||
|
||||
<Listbox
|
||||
label="Select a table to reference to"
|
||||
value={selectedTable?.id}
|
||||
error={errors.table}
|
||||
onChange={(value: string) => updateSelectedTable(Number(value))}
|
||||
>
|
||||
<Listbox.Option key="empty" value="" label="---">
|
||||
---
|
||||
</Listbox.Option>
|
||||
{sortBy(tables, ['schema']).map((table: PostgresTable) => {
|
||||
return (
|
||||
<Listbox.Option key={table.id} value={table.id} label={table.name}>
|
||||
<div className="flex items-center">
|
||||
{/* For aria searching to target the table name instead of schema */}
|
||||
<Typography.Text className="hidden">{table.name}</Typography.Text>
|
||||
<Typography.Text small className="opacity-50 mr-2">
|
||||
{table.schema}
|
||||
</Typography.Text>
|
||||
{table.name}
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
|
||||
{selectedForeignKey?.table && (
|
||||
<Listbox
|
||||
value={selectedColumn?.id}
|
||||
// @ts-ignore
|
||||
label={
|
||||
<div>
|
||||
Select a column from{' '}
|
||||
<Typography.Text code>{selectedForeignKey?.table}</Typography.Text> to reference to
|
||||
</div>
|
||||
}
|
||||
error={errors.column}
|
||||
onChange={(value: string) => updateSelectedColumn(value)}
|
||||
label="Select a table to reference to"
|
||||
value={selectedTable?.id}
|
||||
error={errors.table}
|
||||
onChange={(value: string) => updateSelectedTable(Number(value))}
|
||||
>
|
||||
{(selectedTable?.columns ?? []).map((column: PostgresColumn) => (
|
||||
<Listbox.Option key={column.id} value={column.id} label={column.name}>
|
||||
<div className="flex items-center">
|
||||
{column.name}
|
||||
<Typography.Text small className="opacity-50 ml-2">
|
||||
{column.format}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
<Listbox.Option key="empty" value="" label="---">
|
||||
---
|
||||
</Listbox.Option>
|
||||
{sortBy(tables, ['schema']).map((table: PostgresTable) => {
|
||||
return (
|
||||
<Listbox.Option key={table.id} value={table.id} label={table.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* For aria searching to target the table name instead of schema */}
|
||||
<span className="hidden">{table.name}</span>
|
||||
<span className="text-scale-900">{table.schema}</span>
|
||||
<span className="text-scale-1200">{table.name}</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedForeignKey?.table && (
|
||||
<Listbox
|
||||
value={selectedColumn?.id}
|
||||
// @ts-ignore
|
||||
label={
|
||||
<div>
|
||||
Select a column from{' '}
|
||||
<Typography.Text code>{selectedForeignKey?.table}</Typography.Text> to reference
|
||||
to
|
||||
</div>
|
||||
}
|
||||
error={errors.column}
|
||||
onChange={(value: string) => updateSelectedColumn(value)}
|
||||
>
|
||||
{(selectedTable?.columns ?? []).map((column: PostgresColumn) => (
|
||||
<Listbox.Option key={column.id} value={column.id} label={column.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-scale-1200">{column.name}</span>
|
||||
<span className="text-scale-900">{column.format}</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
)}
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const HeaderTitle: FC<Props> = ({ isNewRecord, tableName }) => {
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
{tableName && <Typography.Text code>{tableName}</Typography.Text>}
|
||||
{tableName && <span className="text-code">{tableName}</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-3
@@ -50,11 +50,10 @@ const JsonEdit: FC<JsonEditProps> = ({ column, jsonString, visible, closePanel,
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
title={'JSON'}
|
||||
size="large"
|
||||
header={'JSON'}
|
||||
visible={visible}
|
||||
onCancel={closePanel}
|
||||
onConfirm={validateJSON}
|
||||
customFooter={<ActionBar closePanel={closePanel} applyFunction={validateJSON} />}
|
||||
>
|
||||
<TwoOptionToggle
|
||||
|
||||
+25
-29
@@ -1,5 +1,5 @@
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { Space, SidePanel, Typography, IconLoader, IconXCircle } from '@supabase/ui'
|
||||
import { SidePanel, IconLoader, IconXCircle } from '@supabase/ui'
|
||||
import { PostgresRelationship } from '@supabase/postgres-meta'
|
||||
|
||||
import ActionBar from '../ActionBar'
|
||||
@@ -33,45 +33,41 @@ const ReferenceRowViewer: FC<Props> = ({ visible, referenceRow, closePanel }) =>
|
||||
return (
|
||||
<SidePanel
|
||||
visible={visible}
|
||||
wide
|
||||
align="right"
|
||||
// @ts-ignore
|
||||
title={
|
||||
size="large"
|
||||
header={
|
||||
<div>
|
||||
Viewing reference row from{' '}
|
||||
<Typography.Text code>
|
||||
<code className="text-sm">
|
||||
{foreignKey?.target_table_schema ?? ''}.{foreignKey?.target_table_name ?? ''}
|
||||
</Typography.Text>
|
||||
</code>
|
||||
</div>
|
||||
}
|
||||
hideFooter={false}
|
||||
onCancel={closePanel}
|
||||
customFooter={<ActionBar backButtonLabel="Close" closePanel={closePanel} />}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-2 h-full">
|
||||
<Typography>
|
||||
<SidePanel.Content>
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-2 h-full">
|
||||
<IconLoader className="animate-spin" />
|
||||
</Typography>
|
||||
<Typography.Text className="opacity-50">Loading reference row</Typography.Text>
|
||||
</div>
|
||||
) : !row ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-2 h-full">
|
||||
<Typography>
|
||||
<p className="text-sm text-scale-1100">Loading reference row</p>
|
||||
</div>
|
||||
) : !row ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-2 h-full">
|
||||
<IconXCircle />
|
||||
</Typography>
|
||||
<Typography.Text className="opacity-50">
|
||||
Unable to find the corresponding row in {foreignKey?.target_table_schema}.
|
||||
{foreignKey?.target_table_name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
{rowFields.map((field: RowField) => {
|
||||
return <InputField key={field.id} field={field} isEditable={false} errors={{}} />
|
||||
})}
|
||||
</Space>
|
||||
)}
|
||||
<p className="text-sm text-scale-1100">
|
||||
Unable to find the corresponding row in {foreignKey?.target_table_schema}.
|
||||
{foreignKey?.target_table_name}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{rowFields.map((field: RowField) => {
|
||||
return <InputField key={field.id} field={field} isEditable={false} errors={{}} />
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
+43
-41
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { isUndefined, partition, isEmpty } from 'lodash'
|
||||
import { Divider, SidePanel, Space, Typography } from '@supabase/ui'
|
||||
import { SidePanel } from '@supabase/ui'
|
||||
import { Dictionary, Query } from '@supabase/grid'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
@@ -138,16 +138,14 @@ const RowEditor: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
size="large"
|
||||
key="RowEditor"
|
||||
visible={visible}
|
||||
// @ts-ignore
|
||||
title={<HeaderTitle isNewRecord={isNewRecord} tableName={selectedTable.name} />}
|
||||
header={<HeaderTitle isNewRecord={isNewRecord} tableName={selectedTable.name} />}
|
||||
className={`transition-all ease-in duration-100 ${
|
||||
isEditingJson || isViewingReferenceRow ? ' mr-32' : ''
|
||||
}`}
|
||||
onCancel={closePanel}
|
||||
onConfirm={(resolve: () => void) => onSaveChanges(resolve)}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Cancel"
|
||||
@@ -157,45 +155,49 @@ const RowEditor: FC<Props> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%', marginBottom: '2rem' }}>
|
||||
{requiredFields.map((field: RowField) => {
|
||||
return (
|
||||
<InputField
|
||||
key={field.id}
|
||||
field={field}
|
||||
errors={errors}
|
||||
onUpdateField={onUpdateField}
|
||||
onEditJson={setSelectedValueForJsonEdit}
|
||||
onViewForeignKey={() => onViewForeignKey(field)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{optionalFields.length > 0 && (
|
||||
<>
|
||||
<Divider light className="mb-4" />
|
||||
<div>
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
{requiredFields.map((field: RowField) => {
|
||||
return (
|
||||
<InputField
|
||||
key={field.id}
|
||||
field={field}
|
||||
errors={errors}
|
||||
onUpdateField={onUpdateField}
|
||||
onEditJson={setSelectedValueForJsonEdit}
|
||||
onViewForeignKey={() => onViewForeignKey(field)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
{optionalFields.length > 0 && (
|
||||
<>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
<div>
|
||||
<Typography.Text>Optional Fields</Typography.Text>
|
||||
<h3 className="text-base text-scale-1200">Optional Fields</h3>
|
||||
<p className="text-sm text-scale-900">
|
||||
These are columns that do not need any value
|
||||
</p>
|
||||
</div>
|
||||
<Typography.Text type="secondary">
|
||||
These are columns that do not need any value
|
||||
</Typography.Text>
|
||||
{optionalFields.map((field: RowField) => {
|
||||
return (
|
||||
<InputField
|
||||
key={field.id}
|
||||
field={field}
|
||||
errors={errors}
|
||||
onUpdateField={onUpdateField}
|
||||
onEditJson={setSelectedValueForJsonEdit}
|
||||
onViewForeignKey={() => onViewForeignKey(field)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{optionalFields.map((field: RowField) => {
|
||||
return (
|
||||
<InputField
|
||||
key={field.id}
|
||||
field={field}
|
||||
errors={errors}
|
||||
onUpdateField={onUpdateField}
|
||||
onEditJson={setSelectedValueForJsonEdit}
|
||||
onViewForeignKey={() => onViewForeignKey(field)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</SidePanel.Content>
|
||||
</>
|
||||
)}
|
||||
|
||||
<JsonEdit
|
||||
visible={isEditingJson}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { ImportContent } from './TableEditor/TableEditor.types'
|
||||
import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from './SidePanelEditor.types'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
|
||||
import { Modal } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
selectedSchema: string
|
||||
selectedTable?: PostgresTable
|
||||
@@ -258,8 +260,7 @@ const SidePanelEditor: FC<Props> = ({
|
||||
/>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
title="Confirm to close"
|
||||
description="There are unsaved changes. Are you sure you want to close the panel? Your changes will be lost."
|
||||
header="Confirm to close"
|
||||
buttonLabel="Confirm"
|
||||
onSelectCancel={() => setIsClosingPanel(false)}
|
||||
onSelectConfirm={() => {
|
||||
@@ -267,6 +268,14 @@ const SidePanelEditor: FC<Props> = ({
|
||||
setIsEdited(false)
|
||||
closePanel()
|
||||
}}
|
||||
children={
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
+129
-69
@@ -9,13 +9,16 @@ import {
|
||||
Popover,
|
||||
IconLink,
|
||||
IconSettings,
|
||||
Button,
|
||||
Listbox,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { ColumnField, EnumType } from '../SidePanelEditor.types'
|
||||
import { ColumnField, EnumType, PostgresDataTypeOption } from '../SidePanelEditor.types'
|
||||
import ColumnType from '../ColumnEditor/ColumnType'
|
||||
import InputWithSuggestions from '../ColumnEditor/InputWithSuggestions'
|
||||
import { Suggestion } from '../ColumnEditor/ColumnEditor.types'
|
||||
import { typeExpressionSuggestions } from '../ColumnEditor/ColumnEditor.constants'
|
||||
import { POSTGRES_DATA_TYPE_OPTIONS } from '../SidePanelEditor.constants'
|
||||
|
||||
/**
|
||||
* [Joshen] For context:
|
||||
@@ -58,6 +61,13 @@ const Column: FC<Props> = ({
|
||||
}) => {
|
||||
const suggestions: Suggestion[] = typeExpressionSuggestions?.[column.format] ?? []
|
||||
|
||||
let settingsCount = 0
|
||||
|
||||
column.isNullable ? (settingsCount += 1) : null
|
||||
column.isIdentity ? (settingsCount += 1) : null
|
||||
column.isUnique ? (settingsCount += 1) : null
|
||||
column.isArray ? (settingsCount += 1) : null
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center">
|
||||
<div className={`w-[5%] ${!isNewRecord ? 'hidden' : ''}`}>
|
||||
@@ -68,39 +78,60 @@ const Column: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[25%]">
|
||||
<div className="flex items-center justify-between w-[95%] border border-gray-500 rounded-md">
|
||||
<div className="flex items-center justify-between w-[95%]">
|
||||
<Input
|
||||
value={column.name}
|
||||
size="small"
|
||||
disabled={hasImportContent}
|
||||
className={`table-editor-columns-input bg-white dark:bg-transparent rounded ${
|
||||
className={`table-editor-columns-input bg-white dark:bg-transparent lg:gap-0 ${
|
||||
hasImportContent ? 'opacity-50' : ''
|
||||
}`}
|
||||
actions={
|
||||
<Button
|
||||
type={!isUndefined(column.foreignKey) ? 'secondary' : 'default'}
|
||||
onClick={() => onEditRelation(column)}
|
||||
>
|
||||
<IconLink size={14} strokeWidth={!isUndefined(column.foreignKey) ? 2 : 1} />
|
||||
</Button>
|
||||
}
|
||||
onChange={(event: any) => onUpdateColumn({ name: event.target.value })}
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
border-l p-[9px] rounded-r border-gray-500 cursor-pointer transition
|
||||
${column.isPrimaryKey ? 'hover:bg-gray-500' : 'hover:bg-gray-600'}
|
||||
`}
|
||||
onClick={() => onEditRelation(column)}
|
||||
>
|
||||
<IconLink
|
||||
className={!isUndefined(column.foreignKey) ? 'text-green-400' : ''}
|
||||
size={14}
|
||||
strokeWidth={!isUndefined(column.foreignKey) ? 2 : 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[25%]">
|
||||
<div className="w-[95%]">
|
||||
{/* <Listbox onChange={() => console.log('foo')} defaultValue={1} className="lg:gap-0 ">
|
||||
{POSTGRES_DATA_TYPE_OPTIONS.map((option: PostgresDataTypeOption, i) => {
|
||||
if (i > 8) return
|
||||
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
label={option.name}
|
||||
|
||||
// addOnBefore={() => inferIcon(option.type)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Typography.Text>
|
||||
<p>{option.name}</p>
|
||||
</Typography.Text>
|
||||
<p>
|
||||
<Typography.Text small className="opacity-50">
|
||||
{option.description}
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox> */}
|
||||
<ColumnType
|
||||
value={column.format}
|
||||
enumTypes={enumTypes}
|
||||
size="small"
|
||||
showLabel={false}
|
||||
className="table-editor-column-type"
|
||||
className="table-editor-column-type lg:gap-0 "
|
||||
disabled={!isUndefined(column.foreignKey)}
|
||||
onOptionSelect={(format: string) => {
|
||||
onUpdateColumn({ format, defaultValue: '' })
|
||||
@@ -115,7 +146,7 @@ const Column: FC<Props> = ({
|
||||
size="small"
|
||||
value={column.defaultValue}
|
||||
disabled={column.format.includes('int') && column.isIdentity}
|
||||
className={`bg-white dark:bg-transparent rounded ${
|
||||
className={`bg-white dark:bg-transparent rounded lg:gap-0 ${
|
||||
column.format.includes('int') && column.isIdentity ? 'opacity-50' : ''
|
||||
}`}
|
||||
suggestions={suggestions}
|
||||
@@ -138,58 +169,87 @@ const Column: FC<Props> = ({
|
||||
<div className={`${hasImportContent ? 'w-[10%]' : 'w-[0%]'}`} />
|
||||
<div className="w-[5%] flex justify-end">
|
||||
{(!column.isPrimaryKey || column.format.includes('int')) && (
|
||||
<Popover
|
||||
portalled
|
||||
className="w-80 pointer-events-auto"
|
||||
overlay={[
|
||||
<div className="p-4" key={`${column.id}_configuration`}>
|
||||
{!column.isPrimaryKey && (
|
||||
<Checkbox
|
||||
label="Is Nullable"
|
||||
description="Specify if the column can assume a NULL value if no value is provided"
|
||||
checked={column.isNullable}
|
||||
onChange={() => onUpdateColumn({ isNullable: !column.isNullable })}
|
||||
/>
|
||||
<>
|
||||
<Popover
|
||||
size="xlarge"
|
||||
className="pointer-events-auto"
|
||||
header={
|
||||
<div className="flex items-center justify-center">
|
||||
<h5 className="text-sm text-scale-1200">Extra options</h5>
|
||||
</div>
|
||||
}
|
||||
overlay={[
|
||||
<div className="flex flex-col space-y-1" key={`${column.id}_configuration`}>
|
||||
{!column.isPrimaryKey && (
|
||||
<>
|
||||
<Checkbox
|
||||
label="Is Nullable"
|
||||
description="Specify if the column can assume a NULL value if no value is provided"
|
||||
checked={column.isNullable}
|
||||
className="p-4"
|
||||
onChange={() => onUpdateColumn({ isNullable: !column.isNullable })}
|
||||
/>
|
||||
<Popover.Seperator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isNewRecord && (
|
||||
<>
|
||||
<Checkbox
|
||||
label="Is Unique"
|
||||
description="Enforce if values in the column should be unique across rows"
|
||||
checked={column.isUnique}
|
||||
className="p-4"
|
||||
onChange={() => onUpdateColumn({ isUnique: !column.isUnique })}
|
||||
/>
|
||||
<Popover.Seperator />
|
||||
</>
|
||||
)}
|
||||
{column.format.includes('int') && (
|
||||
<>
|
||||
<Checkbox
|
||||
label="Is Identity"
|
||||
description="Automatically assign a sequential unique number to the column"
|
||||
checked={column.isIdentity}
|
||||
className="p-4"
|
||||
onChange={() => {
|
||||
const isIdentity = !column.isIdentity
|
||||
const isArray = isIdentity ? false : column.isArray
|
||||
onUpdateColumn({ isIdentity, isArray })
|
||||
}}
|
||||
/>
|
||||
<Popover.Seperator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!column.isPrimaryKey && (
|
||||
<Checkbox
|
||||
label="Define as Array"
|
||||
description="Define your column as a variable-length multidimensional array"
|
||||
checked={column.isArray}
|
||||
className="p-4"
|
||||
onChange={() => {
|
||||
const isArray = !column.isArray
|
||||
const isIdentity = isArray ? false : column.isIdentity
|
||||
onUpdateColumn({ isArray, isIdentity })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>,
|
||||
]}
|
||||
>
|
||||
<div className="group flex -space-x-1 items-center">
|
||||
{settingsCount > 0 && (
|
||||
<div className="py-0.5 px-2 text-xs rounded-full bg-scale-1200 dark:bg-scale-100 text-scale-100 dark:text-scale-1100">
|
||||
{settingsCount}
|
||||
</div>
|
||||
)}
|
||||
{isNewRecord && (
|
||||
<Checkbox
|
||||
label="Is Unique"
|
||||
description="Enforce if values in the column should be unique across rows"
|
||||
checked={column.isUnique}
|
||||
onChange={() => onUpdateColumn({ isUnique: !column.isUnique })}
|
||||
/>
|
||||
)}
|
||||
{column.format.includes('int') && (
|
||||
<Checkbox
|
||||
label="Is Identity"
|
||||
description="Automatically assign a sequential unique number to the column"
|
||||
checked={column.isIdentity}
|
||||
onChange={() => {
|
||||
const isIdentity = !column.isIdentity
|
||||
const isArray = isIdentity ? false : column.isArray
|
||||
onUpdateColumn({ isIdentity, isArray })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!column.isPrimaryKey && (
|
||||
<Checkbox
|
||||
label="Define as Array"
|
||||
description="Define your column as a variable-length multidimensional array"
|
||||
checked={column.isArray}
|
||||
onChange={() => {
|
||||
const isArray = !column.isArray
|
||||
const isIdentity = isArray ? false : column.isIdentity
|
||||
onUpdateColumn({ isArray, isIdentity })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>,
|
||||
]}
|
||||
>
|
||||
<Typography>
|
||||
<IconSettings size={18} strokeWidth={1} />
|
||||
</Typography>
|
||||
</Popover>
|
||||
<div className="text-scale-900 transition-colors group-hover:text-scale-1200">
|
||||
<IconSettings size={18} strokeWidth={1} />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!hasImportContent && (
|
||||
|
||||
+30
-13
@@ -17,6 +17,7 @@ import {
|
||||
DroppableProvided,
|
||||
DraggableProvided,
|
||||
} from 'react-beautiful-dnd'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
import Column from './Column'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
@@ -143,7 +144,7 @@ const ColumnManagement: FC<Props> = ({
|
||||
<>
|
||||
{hasImportContent ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button type="secondary" icon={<IconEdit />} onClick={onSelectImportData}>
|
||||
<Button type="default" icon={<IconEdit />} onClick={onSelectImportData}>
|
||||
Edit content
|
||||
</Button>
|
||||
<Button danger type="outline" icon={<IconTrash />} onClick={onClearImportContent}>
|
||||
@@ -151,7 +152,7 @@ const ColumnManagement: FC<Props> = ({
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="secondary" onClick={onSelectImportData}>
|
||||
<Button type="default" onClick={onSelectImportData}>
|
||||
Import data via spreadsheet
|
||||
</Button>
|
||||
)}
|
||||
@@ -192,21 +193,37 @@ const ColumnManagement: FC<Props> = ({
|
||||
{/* Drag handle */}
|
||||
{isNewRecord && <div className="w-[5%]" />}
|
||||
<div className="w-[25%]">
|
||||
<Typography.Text small>Name</Typography.Text>
|
||||
<h5 className="text-xs text-scale-900">Name</h5>
|
||||
</div>
|
||||
<div className="w-[25%]">
|
||||
<Typography.Text small>Type</Typography.Text>
|
||||
<h5 className="text-xs text-scale-900">Type</h5>
|
||||
</div>
|
||||
<div className={`${isNewRecord ? 'w-[25%]' : 'w-[30%]'} flex items-center space-x-2`}>
|
||||
<Typography.Text small>Default Value</Typography.Text>
|
||||
<div>
|
||||
<Typography.Text>
|
||||
<IconHelpCircle size={15} strokeWidth={1.5} />
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<h5 className="text-xs text-scale-900">Default Value</h5>
|
||||
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<h5 className="text-xs text-scale-900">
|
||||
<IconHelpCircle size={15} strokeWidth={1.5} />
|
||||
</h5>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 shadow py-1 px-2 rounded leading-none', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-scale-1200 text-xs">
|
||||
Can be either a value or a SQL expression
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<div className="w-[10%]">
|
||||
<Typography.Text small>Primary</Typography.Text>
|
||||
<h5 className="text-xs text-scale-900">Primary</h5>
|
||||
</div>
|
||||
{/* Empty space */}
|
||||
<div className={`${hasImportContent ? 'w-[10%]' : 'w-0'}`} />
|
||||
@@ -222,7 +239,7 @@ const ColumnManagement: FC<Props> = ({
|
||||
{(droppableProvided: DroppableProvided) => (
|
||||
<div
|
||||
ref={droppableProvided.innerRef}
|
||||
className={`space-y-2 bg-gray-200 dark:bg-bg-alt-dark rounded-md px-3 py-2 ${
|
||||
className={`space-y-2 bg-gray-500 rounded-md px-3 py-2 ${
|
||||
isNewRecord ? '' : '-mx-3'
|
||||
}`}
|
||||
>
|
||||
@@ -291,7 +308,7 @@ const ColumnManagement: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{!hasImportContent && (
|
||||
<Button type="secondary" onClick={() => onAddColumn()}>
|
||||
<Button type="default" onClick={() => onAddColumn()}>
|
||||
Add column
|
||||
</Button>
|
||||
)}
|
||||
|
||||
+12
-14
@@ -32,20 +32,18 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.Text>
|
||||
<p className="mb-3">
|
||||
<div className="space-y-10">
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100 mb-2">
|
||||
Upload a CSV or TSV file. The first row should be the headers of the table, and your
|
||||
headers should not include any special characters other than hyphens (
|
||||
<Typography.Text code>-</Typography.Text>) or underscores (
|
||||
<Typography.Text code>_</Typography.Text>).
|
||||
<span className="text-code">-</span>) or underscores (<span className="text-code">_</span>
|
||||
).
|
||||
</p>
|
||||
<Typography.Text type="secondary">
|
||||
<p className="text-sm opacity-50 mb-3">
|
||||
Tip: Datetime columns should be formatted as YYYY-MM-DD HH:mm:ss
|
||||
</p>
|
||||
</Typography.Text>
|
||||
</Typography.Text>
|
||||
<p className="text-xs text-scale-900">
|
||||
Tip: Datetime columns should be formatted as YYYY-MM-DD HH:mm:ss
|
||||
</p>
|
||||
</div>
|
||||
{!uploadedFile ? (
|
||||
<div
|
||||
className={`flex items-center justify-center border dark:border-gray-500 border-dashed rounded-md h-48 ${
|
||||
@@ -64,8 +62,8 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center border dark:border-gray-500 border-dashed rounded-md h-32 space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconFileText size={16} strokeWidth={2} />
|
||||
<Typography.Title level={4}>{uploadedFile.name}</Typography.Title>
|
||||
<IconFileText size={14} strokeWidth={1.5} />
|
||||
<h3 className="text-scale-1200 text-base">{uploadedFile.name}</h3>
|
||||
</div>
|
||||
{parseProgress === 100 ? (
|
||||
<Button type="outline" onClick={removeUploadedFile}>
|
||||
@@ -87,7 +85,7 @@ const SpreadSheetFileUpload: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
<input ref={uploadButtonRef} className="hidden" type="file" onChange={onFileUpload} />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+11
-10
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import { Input, Typography } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
input: string
|
||||
@@ -7,26 +7,27 @@ interface Props {
|
||||
}
|
||||
|
||||
const SpreadSheetTextInput: FC<Props> = ({ input, onInputChange }) => (
|
||||
<>
|
||||
<Typography.Text>
|
||||
<p className="mb-3">
|
||||
<div className="space-y-10">
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100 mb-2">
|
||||
Copy a table from a spreadsheet program such as Google Sheets or Excel and paste it in the
|
||||
field below. The first row should be the headers of the table, and your headers should not
|
||||
include any special characters other than hyphens (<Typography.Text code>-</Typography.Text>
|
||||
) or underscores (<Typography.Text code>_</Typography.Text>).
|
||||
</p>
|
||||
<p className="text-sm opacity-50 mb-3">
|
||||
<p className="text-xs text-scale-900">
|
||||
Tip: Datetime columns should be formatted as YYYY-MM-DD HH:mm:ss
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<textarea
|
||||
className="font-mono w-full rounded-md bg-gray-700 text-white p-4 text-sm"
|
||||
</div>
|
||||
<Input.TextArea
|
||||
size="tiny"
|
||||
className="font-mono"
|
||||
rows={15}
|
||||
style={{ resize: 'none' }}
|
||||
value={input}
|
||||
onChange={onInputChange}
|
||||
></textarea>
|
||||
</>
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default SpreadSheetTextInput
|
||||
+72
-72
@@ -123,15 +123,11 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
size="large"
|
||||
visible={visible}
|
||||
align="right"
|
||||
title="Add content to new table"
|
||||
onCancel={(event: any) => {
|
||||
// Only close if specifically hit the X button, this is to have the
|
||||
// side panel work with toast messages (clicking on toast will close the panel)
|
||||
if (event?.target) closePanel()
|
||||
}}
|
||||
header="Add content to new table"
|
||||
onCancel={() => closePanel()}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Cancel"
|
||||
@@ -146,81 +142,85 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Tabs block>
|
||||
{/* @ts-ignore */}
|
||||
<Tabs.Panel id="fileUpload" label="Upload CSV">
|
||||
<SpreadSheetFileUpload
|
||||
parseProgress={parseProgress}
|
||||
uploadedFile={uploadedFile}
|
||||
onFileUpload={onFileUpload}
|
||||
removeUploadedFile={resetSpreadsheetImport}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
{/* @ts-ignore */}
|
||||
<Tabs.Panel id="pasteText" label="Paste text">
|
||||
<SpreadSheetTextInput input={input} onInputChange={onInputChange} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
<SidePanel.Content>
|
||||
<div className="flex flex-col">
|
||||
<Tabs block type="pills">
|
||||
<Tabs.Panel id="fileUpload" label="Upload CSV">
|
||||
<SpreadSheetFileUpload
|
||||
parseProgress={parseProgress}
|
||||
uploadedFile={uploadedFile}
|
||||
onFileUpload={onFileUpload}
|
||||
removeUploadedFile={resetSpreadsheetImport}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="pasteText" label="Paste text">
|
||||
<SpreadSheetTextInput input={input} onInputChange={onInputChange} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{spreadsheetData.headers.length > 0 && (
|
||||
<div className="py-5 space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Typography.Text>Content Preview</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
Your table will have {spreadsheetData.rowCount.toLocaleString()} rows and the
|
||||
following {spreadsheetData.headers.length} columns.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SpreadsheetPreview headers={spreadsheetData.headers} />
|
||||
</div>
|
||||
{errors.length > 0 && (
|
||||
{spreadsheetData.headers.length > 0 && (
|
||||
<div className="py-5 space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Typography.Text>Issues found in spreadsheet</Typography.Text>
|
||||
<Typography.Text>Content Preview</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
Your table can still be created nonetheless despite issues in the following rows.
|
||||
Your table will have {spreadsheetData.rowCount.toLocaleString()} rows and the
|
||||
following {spreadsheetData.headers.length} columns.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SpreadsheetPreview headers={spreadsheetData.headers} />
|
||||
</div>
|
||||
{errors.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{errors.map((error: any, idx: number) => {
|
||||
const key = `import-error-${idx}`
|
||||
const isExpanded = expandedErrors.includes(key)
|
||||
return (
|
||||
<div key={key} className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconChevronRight
|
||||
className={`cursor-pointer transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
size={14}
|
||||
onClick={() => onSelectExpandError(key)}
|
||||
/>
|
||||
<Typography.Text className="w-14">Row: {error.row}</Typography.Text>
|
||||
<Typography.Text>{error.message}</Typography.Text>
|
||||
{error.data?.__parsed_extra && (
|
||||
<>
|
||||
<IconArrowRight size={14} />
|
||||
<Typography.Text>Extra field(s):</Typography.Text>
|
||||
{error.data?.__parsed_extra.map((value: any) => (
|
||||
<Typography.Text code small>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Typography.Text>Issues found in spreadsheet</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
Your table can still be created nonetheless despite issues in the following
|
||||
rows.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{errors.map((error: any, idx: number) => {
|
||||
const key = `import-error-${idx}`
|
||||
const isExpanded = expandedErrors.includes(key)
|
||||
return (
|
||||
<div key={key} className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconChevronRight
|
||||
className={`cursor-pointer transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
size={14}
|
||||
onClick={() => onSelectExpandError(key)}
|
||||
/>
|
||||
<Typography.Text className="w-14">Row: {error.row}</Typography.Text>
|
||||
<Typography.Text>{error.message}</Typography.Text>
|
||||
{error.data?.__parsed_extra && (
|
||||
<>
|
||||
<IconArrowRight size={14} />
|
||||
<Typography.Text>Extra field(s):</Typography.Text>
|
||||
{error.data?.__parsed_extra.map((value: any) => (
|
||||
<Typography.Text code small>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<SpreadsheetPreview
|
||||
headers={spreadsheetData.headers}
|
||||
rows={[error.data]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<SpreadsheetPreview headers={spreadsheetData.headers} rows={[error.data]} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
+87
-73
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { isUndefined, isEmpty } from 'lodash'
|
||||
import { Badge, Checkbox, Divider, SidePanel, Space, Input, Typography } from '@supabase/ui'
|
||||
import { Badge, Checkbox, SidePanel, Input } from '@supabase/ui'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
generateTableFieldFromPostgresTable,
|
||||
formatImportedContentToColumnFields,
|
||||
} from './TableEditor.utils'
|
||||
import SidePanelEditor from '..'
|
||||
|
||||
interface Props {
|
||||
table?: PostgresTable
|
||||
@@ -131,14 +132,14 @@ const TableEditor: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
wide
|
||||
size="large"
|
||||
key="TableEditor"
|
||||
visible={visible}
|
||||
// @ts-ignore
|
||||
title={<HeaderTitle table={table} isDuplicating={isDuplicating} />}
|
||||
header={<HeaderTitle table={table} isDuplicating={isDuplicating} />}
|
||||
className={`transition-all ease-in duration-100 ${isImportingSpreadsheet ? ' mr-32' : ''}`}
|
||||
onCancel={closePanel}
|
||||
onConfirm={(resolve: () => void) => onSaveChanges(resolve)}
|
||||
onConfirm={() => (resolve: () => void) => onSaveChanges(resolve)}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Cancel"
|
||||
@@ -148,78 +149,91 @@ const TableEditor: FC<Props> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Input
|
||||
label="Name"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
error={errors.name}
|
||||
value={tableFields?.name}
|
||||
onChange={(event: any) => onUpdateField({ name: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Description"
|
||||
placeholder="Optional"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
value={tableFields?.comment ?? ''}
|
||||
onChange={(event: any) => onUpdateField({ comment: event.target.value })}
|
||||
/>
|
||||
<div />
|
||||
<Checkbox
|
||||
id="enable-rls"
|
||||
// @ts-ignore
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text>Enable Row Level Security (RLS)</Typography.Text>
|
||||
<Badge color="green">Recommended</Badge>
|
||||
</div>
|
||||
}
|
||||
description="Restrict access to your table by enabling RLS and writing Postgres policies. If RLS is not enabled, anyone with the anon key can modify and delete your data."
|
||||
checked={tableFields?.isRLSEnabled}
|
||||
onChange={() => onUpdateField({ isRLSEnabled: !tableFields?.isRLSEnabled })}
|
||||
size="medium"
|
||||
/>
|
||||
<Divider light />
|
||||
{!isDuplicating && (
|
||||
<ColumnManagement
|
||||
table={{ name: tableFields.name, schema: selectedSchema }}
|
||||
tables={tables}
|
||||
columns={tableFields?.columns}
|
||||
enumTypes={enumTypes}
|
||||
isNewRecord={isNewRecord}
|
||||
importContent={importContent}
|
||||
onColumnsUpdated={(columns) => onUpdateField({ columns })}
|
||||
onSelectImportData={() => setIsImportingSpreadsheet(true)}
|
||||
onClearImportContent={() => {
|
||||
onUpdateField({ columns: DEFAULT_COLUMNS })
|
||||
setImportContent(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isDuplicating && (
|
||||
<>
|
||||
<>
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
<Input
|
||||
label="Name"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
error={errors.name}
|
||||
value={tableFields?.name}
|
||||
onChange={(event: any) => onUpdateField({ name: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label="Description"
|
||||
placeholder="Optional"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
value={tableFields?.comment ?? ''}
|
||||
onChange={(event: any) => onUpdateField({ comment: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
<Checkbox
|
||||
id="duplicate-rows"
|
||||
label="Duplicate table entries"
|
||||
description="This will copy all the data in the table into the new table"
|
||||
checked={isDuplicateRows}
|
||||
onChange={() => setIsDuplicateRows(!isDuplicateRows)}
|
||||
id="enable-rls"
|
||||
// @ts-ignore
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>Enable Row Level Security (RLS)</span>
|
||||
<Badge color="gray">Recommended</Badge>
|
||||
</div>
|
||||
}
|
||||
description="Restrict access to your table by enabling RLS and writing Postgres policies"
|
||||
checked={tableFields?.isRLSEnabled}
|
||||
onChange={() => onUpdateField({ isRLSEnabled: !tableFields?.isRLSEnabled })}
|
||||
size="medium"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
<SpreadsheetImport
|
||||
visible={isImportingSpreadsheet}
|
||||
headers={importContent?.headers}
|
||||
rows={importContent?.rows}
|
||||
saveContent={(prefillData: ImportContent) => {
|
||||
setImportContent(prefillData)
|
||||
setIsImportingSpreadsheet(false)
|
||||
}}
|
||||
closePanel={() => setIsImportingSpreadsheet(false)}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Seperator />
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
{!isDuplicating && (
|
||||
<ColumnManagement
|
||||
table={{ name: tableFields.name, schema: selectedSchema }}
|
||||
tables={tables}
|
||||
columns={tableFields?.columns}
|
||||
enumTypes={enumTypes}
|
||||
isNewRecord={isNewRecord}
|
||||
importContent={importContent}
|
||||
onColumnsUpdated={(columns) => onUpdateField({ columns })}
|
||||
onSelectImportData={() => setIsImportingSpreadsheet(true)}
|
||||
onClearImportContent={() => {
|
||||
onUpdateField({ columns: DEFAULT_COLUMNS })
|
||||
setImportContent(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isDuplicating && (
|
||||
<>
|
||||
<Checkbox
|
||||
id="duplicate-rows"
|
||||
label="Duplicate table entries"
|
||||
description="This will copy all the data in the table into the new table"
|
||||
checked={isDuplicateRows}
|
||||
onChange={() => setIsDuplicateRows(!isDuplicateRows)}
|
||||
size="medium"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SpreadsheetImport
|
||||
visible={isImportingSpreadsheet}
|
||||
headers={importContent?.headers}
|
||||
rows={importContent?.rows}
|
||||
saveContent={(prefillData: ImportContent) => {
|
||||
setImportContent(prefillData)
|
||||
setIsImportingSpreadsheet(false)
|
||||
}}
|
||||
closePanel={() => setIsImportingSpreadsheet(false)}
|
||||
/>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
</>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import NotFoundState from './NotFoundState'
|
||||
import GridHeaderActions from './GridHeaderActions'
|
||||
import SidePanelEditor from './SidePanelEditor'
|
||||
import { SchemaView } from 'components/layouts/TableEditorLayout/TableEditorLayout.types'
|
||||
import { SidePanel } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
/** Theme for the editor */
|
||||
|
||||
@@ -85,7 +85,7 @@ export default WithSidebar
|
||||
|
||||
const LinksWithHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) => {
|
||||
return links.map((x: any) => (
|
||||
<div key={x.heading} className="py-5 border-b dark:border-dark px-3">
|
||||
<div key={x.heading} className="py-5 border-b dark:border-dark px-6">
|
||||
<Menu.Group title={x.heading} />
|
||||
{x.versionLabel && (
|
||||
<div className="px-3 mb-1">
|
||||
@@ -104,7 +104,7 @@ const LinksWithHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) => {
|
||||
}
|
||||
const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) => {
|
||||
return (
|
||||
<ul className="dash-product-menu space-y-1">
|
||||
<ul className="space-y-1">
|
||||
{links.map((x: any, i: number) => {
|
||||
// disable active state for link with subitems
|
||||
const isActive = x.isActive && !subitems
|
||||
|
||||
@@ -12,13 +12,25 @@ const BreadcrumbsView: FC<Props> = ({ defaultValue: breadcrumbs }) => {
|
||||
{breadcrumbs?.length
|
||||
? breadcrumbs.map((breadcrumb: any) => (
|
||||
<Fragment key={breadcrumb.key}>
|
||||
<Typography.Text type="secondary" className="mx-2 ">
|
||||
<IconChevronRight size="small" />
|
||||
</Typography.Text>
|
||||
<span className="text-scale-800 dark:text-scale-700">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16 3.549L7.12 20.600"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<Typography.Text>
|
||||
<a
|
||||
onClick={breadcrumb.onClick || (() => {})}
|
||||
className={`block px-2 py-1 text-sm leading-5 text-gray-400 focus:outline-none focus:bg-gray-100 focus:text-gray-900 ${
|
||||
className={`block px-2 py-1 text-xs leading-5 text-gray-1100 focus:outline-none focus:bg-gray-100 focus:text-gray-900 ${
|
||||
breadcrumb.onClick ? 'cursor-pointer hover:text-white' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
+19
-30
@@ -1,8 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import { Button, IconMessageCircle } from '@supabase/ui'
|
||||
|
||||
import { clickOutsideListener } from 'hooks'
|
||||
import { Button, IconMessageCircle, Popover } from '@supabase/ui'
|
||||
import FeedbackWidget from './FeedbackWidget'
|
||||
|
||||
const FeedbackDropdown = () => {
|
||||
@@ -10,35 +7,18 @@ const FeedbackDropdown = () => {
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [category, setCategory] = useState('Feedback')
|
||||
|
||||
const clickContainerRef = clickOutsideListener(() => {
|
||||
if (isOpen) setIsOpen(false)
|
||||
})
|
||||
|
||||
function onOpen() {
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={clickContainerRef} className="relative inline-block text-left mr-1">
|
||||
<div>
|
||||
<Button
|
||||
onClick={onOpen}
|
||||
type="default"
|
||||
icon={<IconMessageCircle size={16} strokeWidth={2} />}
|
||||
>
|
||||
<span className="block md:hidden">Feedback</span>
|
||||
<span className="hidden md:block">Feedback on this page?</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(e) => setIsOpen(e)}
|
||||
size="content"
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={
|
||||
<FeedbackWidget
|
||||
onClose={() => setIsOpen(false)}
|
||||
setFeedback={setFeedback}
|
||||
@@ -46,8 +26,17 @@ const FeedbackDropdown = () => {
|
||||
category={category}
|
||||
setCategory={setCategory}
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
onClick={onOpen}
|
||||
type="default"
|
||||
icon={<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
|
||||
>
|
||||
Feedback on this page?
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+36
-42
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button, Typography, Divider } from '@supabase/ui'
|
||||
import { Button, Typography, Divider, Input, Popover } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
@@ -47,48 +47,42 @@ const FeedbackWidget = ({ onClose, feedback, setFeedback, category, setCategory
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute origin-top-right right-0 mt-1 w-80 rounded-md shadow-lg z-50">
|
||||
<div className="rounded-md border dark:border-dark bg-bg-primary-light dark:bg-bg-alt-dark">
|
||||
<div className="p-3">
|
||||
<textarea
|
||||
placeholder="Ideas on how to improve this page. Use the Support Form for technical issues."
|
||||
rows={4}
|
||||
ref={inputRef}
|
||||
value={feedback}
|
||||
className="mb-0"
|
||||
onChange={onFeedbackChange}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="p-3 space-y-4 bg-bg-alt-light dark:bg-bg-alt-dark">
|
||||
<div className="flex justify-between space-x-2">
|
||||
<Button type="secondary" onClick={onCancel} className="hover:border-gray-500">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isSending} loading={isSending} onClick={sendFeedback}>
|
||||
Send feedback
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text small type="secondary">
|
||||
<p>
|
||||
Have a technical issue? Contact{' '}
|
||||
<Link href="/support/new">
|
||||
<span className="cursor-pointer transition-colors text-green-600 hover:text-green-700">
|
||||
Supabase Support
|
||||
</span>
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<a href="https://supabase.com/docs" target="_blank">
|
||||
<span className="cursor-pointer transition-colors text-green-600 hover:text-green-700">
|
||||
browse our docs
|
||||
</span>
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="">
|
||||
<Input.TextArea
|
||||
className="w-80 p-3"
|
||||
size="small"
|
||||
placeholder="Ideas on how to improve this page. Use the Support Form for technical issues."
|
||||
rows={5}
|
||||
value={feedback}
|
||||
onChange={onFeedbackChange}
|
||||
/>
|
||||
<Popover.Seperator />
|
||||
<div className="w-80 space-y-3 px-3 py-2 pb-4">
|
||||
<div className="flex justify-between space-x-2">
|
||||
<Button type="default" onClick={onCancel} className="hover:border-gray-500">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isSending} loading={isSending} onClick={sendFeedback}>
|
||||
Send feedback
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-scale-1000">
|
||||
Have a technical issue? Contact{' '}
|
||||
<Link href="/support/new">
|
||||
<a>
|
||||
<span className="cursor-pointer transition-colors text-brand-900 hover:text-brand-1200">
|
||||
Supabase support
|
||||
</span>
|
||||
</a>
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<a href="https://supabase.com/docs" target="_blank">
|
||||
<span className="cursor-pointer transition-colors text-brand-900 hover:text-brand-1200">
|
||||
browse our docs
|
||||
</span>
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
IconHelpCircle,
|
||||
IconMail,
|
||||
IconBookOpen,
|
||||
IconMessageCircle,
|
||||
Popover,
|
||||
Typography,
|
||||
IconBookOpen,
|
||||
} from '@supabase/ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
@@ -24,51 +22,48 @@ const HelpPopover: FC<Props> = () => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="w-80"
|
||||
size="content"
|
||||
align="end"
|
||||
side="bottom"
|
||||
sideOffset={8}
|
||||
overlay={
|
||||
<div className="space-y-4 my-4">
|
||||
<div className="space-y-4 my-4 w-96">
|
||||
<div className="px-5 space-y-4 my-5">
|
||||
<Typography.Title level={5} className={'mb-2'}>
|
||||
Need help with your project?
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary" small>
|
||||
<p>
|
||||
For issues with your project hosted on supabase.com, or other inquiries about our
|
||||
hosted services.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<div className="flex space-x-2">
|
||||
<h5 className="text-scale-1200">Need help with your project?</h5>
|
||||
<p className="text-sm text-scale-900">
|
||||
For issues with your project hosted on supabase.com, or other inquiries about our
|
||||
hosted services.
|
||||
</p>
|
||||
<div className="space-x-2">
|
||||
<Link href={supportUrl}>
|
||||
<Button className="sbui-default-button--dark-white" size="tiny" icon={<IconMail />}>
|
||||
Contact support
|
||||
</Button>
|
||||
<a>
|
||||
<Button type="default" icon={<IconMail />}>
|
||||
Contact support team
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="https://supabase.com/docs/">
|
||||
<Button type="secondary" size="tiny" icon={<IconBookOpen />}>
|
||||
Docs
|
||||
</Button>
|
||||
<a>
|
||||
<Button type="secondary" size="tiny" icon={<IconBookOpen />}>
|
||||
Docs
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Typography.Text type="secondary" small className="block opacity-50">
|
||||
<p className="text-sm text-scale-900">
|
||||
Expected response time is based on your billing tier. Pro and Pay as You Go plans are
|
||||
prioritised.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<Divider light />
|
||||
<Popover.Seperator />
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="px-5 mb-4">
|
||||
<Typography.Title level={5} className={'mb-2'}>
|
||||
Reach out to the community
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary" small className={'block mb-2'}>
|
||||
<p>
|
||||
For other support, including questions on our client libraries, advice, or best
|
||||
practices.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<h5 className={'mb-2'}>Reach out to the community</h5>
|
||||
|
||||
<p className="text-sm text-scale-900">
|
||||
For other support, including questions on our client libraries, advice, or best
|
||||
practices.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div
|
||||
@@ -78,7 +73,7 @@ const HelpPopover: FC<Props> = () => {
|
||||
<a
|
||||
href="https://discord.supabase.com"
|
||||
target="_blank"
|
||||
className="block cursor-pointer"
|
||||
className="block cursor-pointer dark"
|
||||
>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
@@ -88,8 +83,7 @@ const HelpPopover: FC<Props> = () => {
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button
|
||||
className="sbui-default-button--dark-white"
|
||||
type="default"
|
||||
type="secondary"
|
||||
icon={<SVG src={`/img/discord-icon.svg`} className="w-4 h-4" />}
|
||||
>
|
||||
<span style={{ color: '#404EED' }}>Join Discord server</span>
|
||||
@@ -111,7 +105,7 @@ const HelpPopover: FC<Props> = () => {
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button type="default" icon={<IconMessageCircle />}>
|
||||
<Button type="secondary" icon={<IconMessageCircle />}>
|
||||
GitHub Discussions
|
||||
</Button>
|
||||
</a>
|
||||
@@ -121,7 +115,11 @@ const HelpPopover: FC<Props> = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button as="span" type="text" icon={<IconHelpCircle size={16} strokeWidth={2} />}>
|
||||
<Button
|
||||
as="span"
|
||||
type="default"
|
||||
icon={<IconHelpCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</Popover>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Link from 'next/link'
|
||||
import { IconChevronRight, Typography } from '@supabase/ui'
|
||||
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
@@ -15,49 +14,63 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
const { selectedOrganization, selectedProject } = ui
|
||||
|
||||
return (
|
||||
<div className={`p-2 max-h-12 h-12 ${headerBorder ? 'border-b dark:border-dark' : ''}`}>
|
||||
<div className="PageHeader">
|
||||
<div className="Breadcrumbs flex justify-between">
|
||||
<div className="text-sm flex items-center">
|
||||
{/* Organization is selected */}
|
||||
{selectedOrganization ? (
|
||||
<>
|
||||
{/* Org Dropdown */}
|
||||
<OrgDropdown />
|
||||
<div
|
||||
className={`py-2 px-5 max-h-12 h-12 flex justify-between ${
|
||||
headerBorder ? 'border-b dark:border-dark' : ''
|
||||
}`}
|
||||
>
|
||||
{/* <div className="PageHeader"> */}
|
||||
{/* <div className="flex justify-between"> */}
|
||||
<div className="text-sm flex items-center -ml-2">
|
||||
{/* Organization is selected */}
|
||||
{selectedOrganization ? (
|
||||
<>
|
||||
{/* Org Dropdown */}
|
||||
<OrgDropdown />
|
||||
|
||||
{/* Project is selected */}
|
||||
{selectedProject && (
|
||||
<>
|
||||
<Typography.Text className="mx-2 ">
|
||||
<IconChevronRight size="tiny" strokeWidth={1} />
|
||||
</Typography.Text>
|
||||
{/* Project Dropdown */}
|
||||
<ProjectDropdown />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text small className="mx-2">
|
||||
<Link href="/">
|
||||
<a
|
||||
className={`block px-2 py-1 focus:outline-none focus:bg-transparent cursor-pointer dark:hover:text-white`}
|
||||
{/* Project is selected */}
|
||||
{selectedProject && (
|
||||
<>
|
||||
<span className="text-scale-800 dark:text-scale-700">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision"
|
||||
>
|
||||
Supabase
|
||||
</a>
|
||||
</Link>
|
||||
</Typography.Text>
|
||||
<path d="M16 3.549L7.12 20.600"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{/* Project Dropdown */}
|
||||
<ProjectDropdown />
|
||||
</>
|
||||
)}
|
||||
{/* Additional breadcrumbs are supplied */}
|
||||
<BreadcrumbsView defaultValue={breadcrumbs} />
|
||||
</div>
|
||||
<div className="flex space-x-1 md:space-x-2">
|
||||
{customHeaderComponents && customHeaderComponents}
|
||||
{IS_PLATFORM && <HelpPopover />}
|
||||
{IS_PLATFORM && <FeedbackDropdown />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link href="/">
|
||||
<a
|
||||
className={`text-xs text-scale-1200 px-2 py-1 focus:outline-none focus:bg-transparent cursor-pointer`}
|
||||
>
|
||||
Supabase
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{/* Additional breadcrumbs are supplied */}
|
||||
<BreadcrumbsView defaultValue={breadcrumbs} />
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{customHeaderComponents && customHeaderComponents}
|
||||
{IS_PLATFORM && <HelpPopover />}
|
||||
{IS_PLATFORM && <FeedbackDropdown />}
|
||||
</div>
|
||||
</div>
|
||||
// </div>
|
||||
// </div>
|
||||
)
|
||||
}
|
||||
export default observer(LayoutHeader)
|
||||
@@ -25,7 +25,7 @@ const OrgDropdown = () => {
|
||||
{x.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
<Divider light />
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item icon={<IconPlus size="tiny" />} onClick={() => router.push(`/new`)}>
|
||||
New organization
|
||||
</Dropdown.Item>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Dropdown, Divider, IconPlus } from '@supabase/ui'
|
||||
import { Button, Dropdown, Divider, IconPlus, Popover } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
@@ -31,7 +31,7 @@ const ProjectDropdown = () => {
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
<Divider light />
|
||||
<Popover.Seperator />
|
||||
<Link href={`/new/${selectedOrganizationSlug}`}>
|
||||
<a className="block">
|
||||
<Dropdown.Item icon={<IconPlus size="tiny" />}>New project</Dropdown.Item>
|
||||
|
||||
@@ -3,7 +3,16 @@ import Link from 'next/link'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { Button, Divider, IconHome, Dropdown, IconUser, Select, Typography } from '@supabase/ui'
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
IconHome,
|
||||
Dropdown,
|
||||
IconUser,
|
||||
Select,
|
||||
Typography,
|
||||
IconSettings,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
@@ -22,7 +31,18 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
const otherRoutes = generateOtherRoutes(projectRef)
|
||||
|
||||
return (
|
||||
<div className="h-screen w-14 flex flex-col justify-between p-2 overflow-y-hidden bg-sidebar-light dark:bg-sidebar-dark border-r dark:border-dark">
|
||||
<div
|
||||
className="
|
||||
h-screen w-14
|
||||
flex flex-col
|
||||
justify-between
|
||||
p-2
|
||||
overflow-y-hidden
|
||||
bg-sidebar-light
|
||||
dark:bg-sidebar-dark
|
||||
border-r
|
||||
dark:border-dark"
|
||||
>
|
||||
<ul className="flex flex-col space-y-2">
|
||||
<Link href={'/'}>
|
||||
<a className="block">
|
||||
@@ -42,7 +62,7 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
link: `/project/${projectRef}`,
|
||||
}}
|
||||
/>
|
||||
<Divider light />
|
||||
<div className="h-px w-full bg-scale-500"></div>
|
||||
{productRoutes.map((route) => (
|
||||
<NavigationIconButton
|
||||
key={route.key}
|
||||
@@ -50,7 +70,7 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
isActive={activeRoute === route.key}
|
||||
/>
|
||||
))}
|
||||
<Divider light />
|
||||
<div className="h-px w-full bg-scale-500"></div>
|
||||
{otherRoutes.map((route) => (
|
||||
<NavigationIconButton
|
||||
key={route.key}
|
||||
@@ -63,33 +83,30 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
<Dropdown
|
||||
side="right"
|
||||
align="start"
|
||||
overlay={[
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
<Dropdown.Item key="header">
|
||||
<Link href="/account/me">
|
||||
<a className="block">
|
||||
<Typography.Text>Account Preferences</Typography.Text>
|
||||
</a>
|
||||
</Link>
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
: []),
|
||||
<Divider key="d1" light />,
|
||||
<Dropdown.Misc key="theme">
|
||||
<div className="w-[240px] py-1 flex items-center justify-between">
|
||||
<Typography.Text>Theme</Typography.Text>
|
||||
<Select
|
||||
value={ui.themeOption}
|
||||
onChange={(e: any) => ui.onThemeOptionChange(e.target.value)}
|
||||
>
|
||||
<Select.Option value="system">System default</Select.Option>
|
||||
<Select.Option value="dark">Dark</Select.Option>
|
||||
<Select.Option value="light">Light</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</Dropdown.Misc>,
|
||||
]}
|
||||
overlay={
|
||||
<>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<Link href="/account/me">
|
||||
<Dropdown.Item key="header" icon={<IconSettings size={14} strokeWidth={1.5} />}>
|
||||
Account Preferences
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Dropdown.Seperator />
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Label>Theme</Dropdown.Label>
|
||||
<Dropdown.RadioGroup
|
||||
key="theme"
|
||||
value={ui.themeOption}
|
||||
onChange={(e: any) => ui.onThemeOptionChange(e)}
|
||||
>
|
||||
<Dropdown.Radio value="system">System default</Dropdown.Radio>
|
||||
<Dropdown.Radio value="dark">Dark</Dropdown.Radio>
|
||||
<Dropdown.Radio value="light">Light</Dropdown.Radio>
|
||||
</Dropdown.RadioGroup>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button as="span" type="text" size="tiny">
|
||||
<div className="py-1">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
import { Route } from 'components/ui/ui.types'
|
||||
@@ -17,10 +16,11 @@ const NavigationIconButton: FC<Props> = ({ route, isActive = false }) => {
|
||||
<Link href={route.link}>
|
||||
<a
|
||||
className={[
|
||||
'transition-colors duration-200',
|
||||
'flex items-center justify-center h-10 w-10 rounded', // Layout
|
||||
'text-gray-600 hover:bg-gray-100', // Light mode
|
||||
'dark:text-gray-400 dark:hover:bg-bg-alt-dark dark:hover:text-white', // Dark mode
|
||||
`${isActive ? 'bg-gray-100 dark:bg-bg-alt-dark dark:!text-white' : ''}`,
|
||||
'bg-scale-200 hover:bg-scale-500', // Light mode
|
||||
'text-scale-900 hover:text-scale-1200 ', // Dark mode
|
||||
`${isActive ? 'bg-scale-500 shadow-sm text-scale-1200' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{route.icon}
|
||||
@@ -31,12 +31,11 @@ const NavigationIconButton: FC<Props> = ({ route, isActive = false }) => {
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'shadow px-2 py-1 rounded border',
|
||||
'bg-white',
|
||||
'dark:bg-gray-800 dark:border-gray-700',
|
||||
'bg-scale-100 shadow py-1 px-2 rounded leading-none', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<Typography.Text small>{route.label}</Typography.Text>
|
||||
<span className="text-scale-1200 text-xs">{route.label}</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useRouter } from 'next/router'
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Divider,
|
||||
Typography,
|
||||
Menu,
|
||||
Input,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
IconTrash,
|
||||
IconChevronDown,
|
||||
} from '@supabase/ui'
|
||||
import Divider from 'components/ui/Divider'
|
||||
import { partition } from 'lodash'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
@@ -85,7 +85,7 @@ const SqlEditorMenu: FC<Props> = ({
|
||||
Getting started
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="dash-product-menu space-y-1">
|
||||
<div className="space-y-1">
|
||||
<Link href={`/project/${projectRef}/sql/templates`}>
|
||||
<div>
|
||||
<Menu.Item rounded active={page === 'templates'}>
|
||||
@@ -106,7 +106,7 @@ const SqlEditorMenu: FC<Props> = ({
|
||||
Favourites
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="dash-product-menu space-y-1">
|
||||
<div className="space-y-1">
|
||||
{favouriteQueries.map((query) => {
|
||||
const id = query.id
|
||||
const isActive = queryId === id
|
||||
@@ -162,7 +162,7 @@ const SqlEditorMenu: FC<Props> = ({
|
||||
SQL snippets
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="dash-product-menu space-y-1">
|
||||
<div className="space-y-1">
|
||||
{savedQueries.map((query) => {
|
||||
const id = query.id
|
||||
const isActive = queryId === id
|
||||
|
||||
@@ -4,7 +4,6 @@ import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Divider,
|
||||
Menu,
|
||||
Input,
|
||||
Typography,
|
||||
@@ -15,6 +14,8 @@ import {
|
||||
IconTrash,
|
||||
IconChevronDown,
|
||||
Loading,
|
||||
Modal,
|
||||
IconEdit2,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
@@ -22,10 +23,10 @@ import { useStore } from 'hooks'
|
||||
import { useProjectContentStore } from 'stores/projectContentStore'
|
||||
import { useSqlStore, TAB_TYPES } from 'localStores/sqlEditor/SqlEditorStore'
|
||||
|
||||
import Modal from 'components/to-be-cleaned/ModalsDeprecated/Modal'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
import RenameQuery from 'components/to-be-cleaned/SqlEditor/RenameQuery'
|
||||
import { createSqlSnippet } from 'components/to-be-cleaned/SqlEditor/SqlEditor.utils'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
|
||||
const OpenQueryItem = observer(({ tabInfo }: { tabInfo: any }) => {
|
||||
const sqlEditorStore: any = useSqlStore()
|
||||
@@ -33,12 +34,13 @@ const OpenQueryItem = observer(({ tabInfo }: { tabInfo: any }) => {
|
||||
const active = sqlEditorStore.activeTab.id === id
|
||||
|
||||
return (
|
||||
<Menu.Item rounded key={id} active={active} onClick={() => sqlEditorStore.selectTab(id)}>
|
||||
<div className="flex">
|
||||
<Typography.Text className="flex-grow truncate flex items-center">{name}</Typography.Text>
|
||||
{active && <DropdownMenu tabInfo={tabInfo} />}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<ProductMenuItem
|
||||
key={id}
|
||||
isActive={active}
|
||||
name={name}
|
||||
action={active && <DropdownMenu tabInfo={tabInfo} />}
|
||||
onClick={() => sqlEditorStore.selectTab(id)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -51,6 +53,8 @@ const DropdownMenu = observer(({ tabInfo }: { tabInfo: any }) => {
|
||||
|
||||
const [tabId, setTabId] = useState('')
|
||||
const [renameModalOpen, setRenameModalOpen] = useState(false)
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
|
||||
const { id, name } = tabInfo || {}
|
||||
|
||||
function onCloseRenameModal() {
|
||||
@@ -62,24 +66,14 @@ const DropdownMenu = observer(({ tabInfo }: { tabInfo: any }) => {
|
||||
setRenameModalOpen(true)
|
||||
}
|
||||
|
||||
async function removeQuery(e: any) {
|
||||
confirmAlert({
|
||||
title: 'Confirm to remove',
|
||||
message: `Are you sure you want to remove '${name}' ?`,
|
||||
onAsyncConfirm: async () => {
|
||||
await contentStore.del(id)
|
||||
await contentStore.load()
|
||||
sqlEditorStore.closeTab(id)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function renderMenu() {
|
||||
return (
|
||||
<>
|
||||
<Dropdown.Item onClick={renameQuery}>Rename query</Dropdown.Item>
|
||||
<Divider light />
|
||||
<Dropdown.Item onClick={removeQuery} icon={<IconTrash size="tiny" />}>
|
||||
<Dropdown.Item onClick={renameQuery} icon={<IconEdit2 size="tiny" />}>
|
||||
Rename query
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item onClick={() => setDeleteModalOpen(true)} icon={<IconTrash size="tiny" />}>
|
||||
Remove query
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
@@ -90,17 +84,40 @@ const DropdownMenu = observer(({ tabInfo }: { tabInfo: any }) => {
|
||||
<div>
|
||||
{IS_PLATFORM ? (
|
||||
<Dropdown side="bottom" align="end" overlay={renderMenu()}>
|
||||
<Button as="span" type="text" icon={<IconChevronDown />} style={{ padding: '3px' }} />
|
||||
<Button
|
||||
as="span"
|
||||
type="text"
|
||||
icon={<IconChevronDown size={12} />}
|
||||
style={{ padding: '3px' }}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button as="span" type="text" style={{ padding: '3px' }} />
|
||||
)}
|
||||
|
||||
{/* @ts-ignore */}
|
||||
<Modal open={renameModalOpen} handleCloseEvent={onCloseRenameModal}>
|
||||
{/* @ts-ignore */}
|
||||
<RenameQuery tabId={tabId} onComplete={onCloseRenameModal} />
|
||||
</Modal>
|
||||
<RenameQuery
|
||||
// @ts-ignore -- @mildtomato not sure what is wrong here
|
||||
visible={renameModalOpen}
|
||||
onCancel={onCloseRenameModal}
|
||||
tabId={tabId}
|
||||
onComplete={onCloseRenameModal}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
header="Confirm to remove"
|
||||
buttonLabel="Confirm"
|
||||
visible={deleteModalOpen}
|
||||
onSelectConfirm={async () => {
|
||||
await contentStore.del(id)
|
||||
await contentStore.load()
|
||||
sqlEditorStore.closeTab(id)
|
||||
}}
|
||||
onSelectCancel={() => setDeleteModalOpen(false)}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="text-sm text-scale-1100 py-4">{`Are you sure you want to remove '${name}' ?`}</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -163,42 +180,36 @@ const SideBarContent = observer(() => {
|
||||
: queries.filter((tab: any) => tab.name.includes(filterString))
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<Menu>
|
||||
<div className="mt-6">
|
||||
<Menu type="pills">
|
||||
{IS_PLATFORM && (
|
||||
<div className="my-4 px-3 space-y-1">
|
||||
<Menu.Misc>
|
||||
<Button
|
||||
block
|
||||
className="mx-1"
|
||||
icon={<IconPlus />}
|
||||
type="text"
|
||||
style={{ justifyContent: 'start' }}
|
||||
onClick={() => handleNewQuery()}
|
||||
loading={loadingNewQuery}
|
||||
>
|
||||
New query
|
||||
</Button>
|
||||
</Menu.Misc>
|
||||
<Menu.Misc>
|
||||
<Input
|
||||
icon={<IconSearch size="tiny" />}
|
||||
className="sbui-input-no-border mx-1"
|
||||
placeholder="Search"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
value={filterString}
|
||||
size="tiny"
|
||||
actions={
|
||||
filterString && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="cursor-pointer mr-2"
|
||||
onClick={() => setFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Menu.Misc>
|
||||
<div className="my-4 px-3 space-y-1 mx-3">
|
||||
<Button
|
||||
block
|
||||
icon={<IconPlus />}
|
||||
type="default"
|
||||
style={{ justifyContent: 'start' }}
|
||||
onClick={() => handleNewQuery()}
|
||||
loading={loadingNewQuery}
|
||||
>
|
||||
New query
|
||||
</Button>
|
||||
<Input
|
||||
icon={<IconSearch size="tiny" />}
|
||||
placeholder="Search"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
value={filterString}
|
||||
size="tiny"
|
||||
actions={
|
||||
filterString && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="cursor-pointer mr-2"
|
||||
onClick={() => setFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -210,19 +221,17 @@ const SideBarContent = observer(() => {
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{IS_PLATFORM && (
|
||||
<div className="px-3 dash-product-menu">
|
||||
<div className="px-3">
|
||||
<Menu.Group title="Getting started" />
|
||||
{getStartedTabs.map((tab: any) => {
|
||||
const { id, name } = tab || {}
|
||||
return (
|
||||
<Menu.Item
|
||||
rounded
|
||||
<ProductMenuItem
|
||||
key={id}
|
||||
active={sqlEditorStore.activeTab.id === id}
|
||||
name={name}
|
||||
isActive={sqlEditorStore.activeTab.id === id}
|
||||
onClick={() => sqlEditorStore.selectTab(id)}
|
||||
>
|
||||
<Typography.Text className="truncate">{name}</Typography.Text>
|
||||
</Menu.Item>
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
Dropdown,
|
||||
Menu,
|
||||
Typography,
|
||||
IconPlus,
|
||||
IconLoader,
|
||||
IconMoreVertical,
|
||||
Alert,
|
||||
IconEdit,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
import { STORAGE_ROW_STATUS } from 'components/to-be-cleaned/Storage/Storage.constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
@@ -37,78 +39,74 @@ const StorageMenu: FC<Props> = () => {
|
||||
} = storageExplorerStore || {}
|
||||
|
||||
return (
|
||||
<div className="my-6 flex flex-col flex-grow space-y-6">
|
||||
<div className="mx-4">
|
||||
<Menu type="pills" className="my-6 flex flex-col flex-grow px-5">
|
||||
<div className="px-2 mb-6">
|
||||
<Button
|
||||
block
|
||||
icon={<IconPlus />}
|
||||
type="text"
|
||||
type="default"
|
||||
icon={
|
||||
<div className="text-scale-900">
|
||||
<IconEdit size={14} />
|
||||
</div>
|
||||
}
|
||||
style={{ justifyContent: 'start' }}
|
||||
onClick={openCreateBucketModal}
|
||||
>
|
||||
New bucket
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mx-4 my-4 space-y-2">
|
||||
<div className="mx-4 w-full flex">
|
||||
<Typography.Text type="secondary" small>
|
||||
All buckets
|
||||
</Typography.Text>
|
||||
<div className="space-y-6">
|
||||
<div className="">
|
||||
<div>
|
||||
<Menu.Group title="All buckets" />
|
||||
{!loaded ? (
|
||||
<div className="py-2 px-4 flex items-center space-x-2">
|
||||
<IconLoader className="animate-spin" size={14} strokeWidth={2} />
|
||||
<span className="text-sm">Loading buckets</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{buckets.length === 0 && (
|
||||
<div className="px-2">
|
||||
<Alert title="No buckets available">
|
||||
Buckets that you create will appear here
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{buckets.map((bucket: any, idx: number) => {
|
||||
const isSelected = bucketId === bucket.id
|
||||
return (
|
||||
<BucketRow
|
||||
key={`${idx}_${bucket.id}`}
|
||||
bucket={bucket}
|
||||
projectRef={ref}
|
||||
isSelected={isSelected}
|
||||
onSelectDeleteBucket={openDeleteBucketModal}
|
||||
onSelectToggleBucketPublic={openToggleBucketPublicModal}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{!loaded ? (
|
||||
<div className="py-2 px-4 flex items-center space-x-2">
|
||||
<IconLoader className="animate-spin" size={12} strokeWidth={2} />
|
||||
<Typography.Text type="secondary">Loading buckets</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{buckets.length === 0 && (
|
||||
<div className="py-2 px-4 flex items-center">
|
||||
<Typography.Text type="secondary">No buckets available</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
{buckets.map((bucket: any, idx: number) => {
|
||||
const isSelected = bucketId === bucket.id
|
||||
return (
|
||||
<BucketRow
|
||||
key={`${idx}_${bucket.id}`}
|
||||
bucket={bucket}
|
||||
projectRef={ref}
|
||||
isSelected={isSelected}
|
||||
onSelectDeleteBucket={openDeleteBucketModal}
|
||||
onSelectToggleBucketPublic={openToggleBucketPublicModal}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-4 flex flex-col space-y-2">
|
||||
<div className="mx-4 space-y-1">
|
||||
<Typography.Text type="secondary" small>
|
||||
Settings
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="dash-product-menu space-y-1">
|
||||
|
||||
<div className="">
|
||||
<Menu.Group title="Settings" />
|
||||
|
||||
<Link href={`/project/${projectRef}/storage/policies`}>
|
||||
<a className="block">
|
||||
<Menu.Item rounded active={page === 'policies'}>
|
||||
<Typography.Text className="truncate">Policies</Typography.Text>
|
||||
</Menu.Item>
|
||||
</a>
|
||||
<Menu.Item rounded active={page === 'policies'}>
|
||||
<Typography.Text className="truncate">Policies</Typography.Text>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
<Link href={`/project/${projectRef}/storage/usage`}>
|
||||
<a className="block">
|
||||
<Menu.Item rounded active={page === 'usage'}>
|
||||
<Typography.Text className="truncate">Usage</Typography.Text>
|
||||
</Menu.Item>
|
||||
</a>
|
||||
<Menu.Item rounded active={page === 'usage'}>
|
||||
<Typography.Text className="truncate">Usage</Typography.Text>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,7 +116,6 @@ const BucketRow = ({
|
||||
bucket = {},
|
||||
projectRef = '',
|
||||
isSelected = false,
|
||||
onSelectBucket = () => {},
|
||||
onSelectDeleteBucket = () => {},
|
||||
onSelectToggleBucketPublic = () => {},
|
||||
}: any) => {
|
||||
@@ -132,46 +129,45 @@ const BucketRow = ({
|
||||
onClick: () => onSelectToggleBucketPublic(bucket),
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="group dash-product-menu">
|
||||
<Menu.Item rounded active={isSelected} onClick={() => onSelectBucket(bucket)}>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Need to investigate, why links here are reopning the entire page */}
|
||||
<Link href={`/project/${projectRef}/storage/buckets/${bucket.id}`}>
|
||||
<a className="block w-full">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text>{bucket.name}</Typography.Text>
|
||||
{bucket.public && <Badge color="yellow">Public</Badge>}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{bucket.status === STORAGE_ROW_STATUS.LOADING && (
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={2} />
|
||||
)}
|
||||
{bucket.status === STORAGE_ROW_STATUS.READY && (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={[
|
||||
bucketOptions.map((option) => (
|
||||
<Dropdown.Item key={option.name} onClick={option.onClick}>
|
||||
{option.name}
|
||||
</Dropdown.Item>
|
||||
)),
|
||||
]}
|
||||
>
|
||||
<Typography.Text>
|
||||
<IconMoreVertical
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Typography.Text>
|
||||
</Dropdown>
|
||||
)}
|
||||
return (
|
||||
<ProductMenuItem
|
||||
key={bucket.id}
|
||||
name={
|
||||
<div className="flex items-center space-x-2">
|
||||
<Typography.Text>{bucket.name}</Typography.Text>
|
||||
{bucket.public && <Badge color="yellow">Public</Badge>}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
}
|
||||
url={`/project/${projectRef}/storage/buckets/${bucket.id}`}
|
||||
isActive={isSelected}
|
||||
action={
|
||||
bucket.status === STORAGE_ROW_STATUS.LOADING ? (
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={2} />
|
||||
) : bucket.status === STORAGE_ROW_STATUS.READY ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={[
|
||||
bucketOptions.map((option) => (
|
||||
<Dropdown.Item key={option.name} onClick={option.onClick}>
|
||||
{option.name}
|
||||
</Dropdown.Item>
|
||||
)),
|
||||
]}
|
||||
>
|
||||
<Typography.Text>
|
||||
<IconMoreVertical
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Typography.Text>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Typography,
|
||||
Listbox,
|
||||
@@ -14,17 +13,17 @@ import {
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconSearch,
|
||||
IconPlus,
|
||||
IconDatabase,
|
||||
IconX,
|
||||
IconLoader,
|
||||
IconRefreshCw,
|
||||
Alert,
|
||||
} from '@supabase/ui'
|
||||
import { PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import Base64 from 'lib/base64'
|
||||
import { useStore } from 'hooks'
|
||||
import { SchemaView } from './TableEditorLayout.types'
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
|
||||
interface Props {
|
||||
selectedSchema?: string
|
||||
@@ -82,31 +81,28 @@ const TableEditorMenu: FC<Props> = ({
|
||||
const schemaTables =
|
||||
searchText.length === 0
|
||||
? tables
|
||||
: tables.filter((table) => table.name.toLowerCase().includes(searchText.toLowerCase()))
|
||||
: // @ts-ignore
|
||||
tables.filter((table) => table.name.toLowerCase().includes(searchText.toLowerCase()))
|
||||
|
||||
const filteredSchemaViews =
|
||||
searchText.length === 0
|
||||
? schemaViews
|
||||
: schemaViews.filter((view) => view.name.includes(searchText))
|
||||
|
||||
// Temp fix - Ideally we'd just take up all the remaining space but
|
||||
// can't seem to figure that out immediately
|
||||
const maxScrollHeight = schemaViews.length > 0 ? 270 : 515
|
||||
|
||||
return (
|
||||
<div className="my-6 flex flex-col flex-grow">
|
||||
<div className="my-6 mx-4 flex flex-col flex-grow space-y-6">
|
||||
{/* Schema selection dropdown */}
|
||||
<div className="mx-4">
|
||||
{!meta.schemas.isInitialized ? (
|
||||
<div className="px-3">
|
||||
{meta.schemas.isLoading ? (
|
||||
<div className="h-[30px] border border-gray-500 px-3 rounded flex items-center space-x-3">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<Typography.Text small>Loading schemas...</Typography.Text>
|
||||
<IconLoader className="animate-spin" size={12} />
|
||||
<span className="text-xs text-scale-900">Loading schemas...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Listbox
|
||||
icon={<IconDatabase size={16} />}
|
||||
size="tiny"
|
||||
value={selectedSchema}
|
||||
// @ts-ignore
|
||||
onChange={(name: string) => {
|
||||
setSearchText('')
|
||||
setSchemaViews([])
|
||||
@@ -114,157 +110,177 @@ const TableEditorMenu: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
{schemas.map((schema) => (
|
||||
<Listbox.Option key={schema.id} value={schema.name} label={schema.name}>
|
||||
{schema.name}
|
||||
<Listbox.Option
|
||||
key={schema.id}
|
||||
value={schema.name}
|
||||
// @ts-ignore
|
||||
label={
|
||||
<>
|
||||
<span className="text-scale-900">schema</span> <span>{schema.name}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span className="text-sm text-scale-900">schema</span>{' '}
|
||||
<span className="text-sm text-scale-1200">{schema.name}</span>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mx-4 my-4 space-y-1">
|
||||
{/* Add new table button */}
|
||||
<div className="space-y-1">
|
||||
{selectedSchema === 'public' && (
|
||||
<Button
|
||||
block
|
||||
icon={<IconPlus />}
|
||||
type="text"
|
||||
style={{ justifyContent: 'start' }}
|
||||
onClick={onAddTable}
|
||||
>
|
||||
New table
|
||||
</Button>
|
||||
<div className="px-3">
|
||||
{/* Add new table button */}
|
||||
<Button
|
||||
block
|
||||
size="tiny"
|
||||
icon={
|
||||
<div className="text-scale-900">
|
||||
<IconEdit size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
type="default"
|
||||
style={{ justifyContent: 'start' }}
|
||||
onClick={onAddTable}
|
||||
>
|
||||
New table
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table search input */}
|
||||
<Input
|
||||
layout="vertical"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
className="sbui-input-no-border"
|
||||
placeholder="Search for a table"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
value={searchText}
|
||||
size="tiny"
|
||||
actions={
|
||||
searchText && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="cursor-pointer mx-1"
|
||||
onClick={() => setSearchText('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="block mb-2 px-3">
|
||||
<Input
|
||||
className="border-none"
|
||||
icon={
|
||||
<div className="text-scale-900">
|
||||
<IconSearch size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
placeholder="Filter tables"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
value={searchText}
|
||||
size="tiny"
|
||||
actions={
|
||||
searchText && (
|
||||
<Button size="tiny" type="text" onClick={() => setSearchText('')}>
|
||||
<IconX size={12} strokeWidth={2} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List of tables belonging to selected schema */}
|
||||
{schemaTables.length > 0 && (
|
||||
<div className="mx-2 mt-6 space-y-2">
|
||||
<div className="px-4 w-full flex items-center justify-between">
|
||||
<Typography.Text type="secondary" small>
|
||||
All tables
|
||||
</Typography.Text>
|
||||
<div className="cursor-pointer" onClick={refreshTables}>
|
||||
<Typography.Text type="secondary">
|
||||
<IconRefreshCw className={isRefreshing ? 'animate-spin' : ''} size={14} />
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto space-y-1" style={{ maxHeight: maxScrollHeight }}>
|
||||
<Menu type="pills">
|
||||
<Menu.Group
|
||||
// @ts-ignore
|
||||
title={
|
||||
<>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<span>All tables</span>
|
||||
<button className="cursor-pointer" onClick={refreshTables}>
|
||||
<IconRefreshCw className={isRefreshing ? 'animate-spin' : ''} size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{schemaTables.map((table) => {
|
||||
const isActive = Number(id) === table.id
|
||||
return (
|
||||
<Link key={table.name} href={`/project/${projectRef}/editor/${table.id}`}>
|
||||
<a className="block editor-product-menu">
|
||||
<Menu.Item rounded active={isActive}>
|
||||
<div className="flex justify-between py-[2px]">
|
||||
<Typography.Text className="truncate flex items-center">
|
||||
{table.name}
|
||||
</Typography.Text>
|
||||
{isActive && (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={[
|
||||
<Dropdown.Item
|
||||
key="edit-table"
|
||||
icon={<IconEdit size="tiny" />}
|
||||
onClick={() => onEditTable(table)}
|
||||
>
|
||||
Edit Table
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item
|
||||
key="duplicate-table"
|
||||
icon={<IconCopy size="tiny" />}
|
||||
onClick={() => onDuplicateTable(table)}
|
||||
>
|
||||
Duplicate Table
|
||||
</Dropdown.Item>,
|
||||
<Divider key="divider" light />,
|
||||
<Dropdown.Item
|
||||
key="delete-table"
|
||||
icon={<IconTrash size="tiny" />}
|
||||
onClick={() => onDeleteTable(table)}
|
||||
>
|
||||
Delete Table
|
||||
</Dropdown.Item>,
|
||||
]}
|
||||
<ProductMenuItem
|
||||
key={table.name}
|
||||
url={`/project/${projectRef}/editor/${table.id}`}
|
||||
name={table.name}
|
||||
isActive={isActive}
|
||||
action={
|
||||
isActive && (
|
||||
<Dropdown
|
||||
size="small"
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={[
|
||||
<Dropdown.Item
|
||||
key="edit-table"
|
||||
icon={<IconEdit size="tiny" />}
|
||||
onClick={() => onEditTable(table)}
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
type="text"
|
||||
icon={<IconChevronDown />}
|
||||
style={{ padding: '3px' }}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</a>
|
||||
</Link>
|
||||
Edit Table
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item
|
||||
key="duplicate-table"
|
||||
icon={<IconCopy size="tiny" />}
|
||||
onClick={() => onDuplicateTable(table)}
|
||||
>
|
||||
Duplicate Table
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Seperator />,
|
||||
<Dropdown.Item
|
||||
key="delete-table"
|
||||
icon={<IconTrash size="tiny" />}
|
||||
onClick={() => onDeleteTable(table)}
|
||||
>
|
||||
Delete Table
|
||||
</Dropdown.Item>,
|
||||
]}
|
||||
>
|
||||
<div className="text-scale-900 transition-colors hover:text-scale-1200">
|
||||
<IconChevronDown size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{/* List of views belonging to selected schema */}
|
||||
{filteredSchemaViews.length > 0 && (
|
||||
<div className="mx-2 mt-6 space-y-2">
|
||||
<div className="px-4 w-full flex items-center justify-between">
|
||||
<Typography.Text type="secondary" small>
|
||||
All Views
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="overflow-y-auto space-y-1" style={{ maxHeight: maxScrollHeight }}>
|
||||
{schemaViews.map((view: SchemaView) => {
|
||||
const viewId = Base64.encode(JSON.stringify(view))
|
||||
const isActive = id === viewId
|
||||
return (
|
||||
<Link key={viewId} href={`/project/${projectRef}/editor/${viewId}`}>
|
||||
<div className="dash-product-menu">
|
||||
<Menu.Item key={viewId} rounded active={isActive}>
|
||||
<div className="flex justify-between">
|
||||
<Typography.Text className="truncate">{view.name}</Typography.Text>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu type="pills">
|
||||
<Menu.Group
|
||||
// @ts-ignore
|
||||
title={
|
||||
<>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<span>All views</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{schemaViews.map((view: SchemaView) => {
|
||||
const viewId = Base64.encode(JSON.stringify(view))
|
||||
const isActive = id === viewId
|
||||
return (
|
||||
<Link key={viewId} href={`/project/${projectRef}/editor/${viewId}`}>
|
||||
<Menu.Item key={viewId} rounded active={isActive}>
|
||||
<div className="flex justify-between">
|
||||
<Typography.Text className="truncate">{view.name}</Typography.Text>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{searchText.length > 0 && schemaTables.length === 0 && filteredSchemaViews.length === 0 && (
|
||||
<div className="my-2 mx-6">
|
||||
<Typography.Text type="secondary">No tables or views found</Typography.Text>
|
||||
<div className="px-3">
|
||||
<Alert title="No tables or views found">This schema has no tables or views</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchText.length === 0 && schemaTables.length === 0 && filteredSchemaViews.length === 0 && (
|
||||
<div className="my-2 mx-6">
|
||||
<Typography.Text type="secondary">No tables available</Typography.Text>
|
||||
<div className="px-3">
|
||||
<Alert title="No tables available">There are no tables in this schema</Alert>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { Divider } from '@supabase/ui'
|
||||
import Divider from 'components/ui/Divider'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
@@ -26,7 +26,7 @@ const VercelIntegrationLayout: FC<Props> = ({ children }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider type="horizontal" light />
|
||||
<Divider light />
|
||||
<div className="flex-grow">
|
||||
<div className="max-w-7xl h-full mx-auto px-4 sm:px-6">
|
||||
<div className="max-w-xl h-full mx-auto py-10">{children}</div>
|
||||
|
||||
@@ -37,28 +37,18 @@ const Header: FC<any> = ({ organization, project }) => {
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<Typography.Text type="secondary">
|
||||
<IconChevronRight size="small" />
|
||||
</Typography.Text>
|
||||
<Typography.Text small>
|
||||
<a>
|
||||
{organization
|
||||
? `Organization: ${organization.name}`
|
||||
: '1. Create an organization'}
|
||||
</a>
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<IconChevronRight size="small" />
|
||||
</Typography.Text>
|
||||
<Typography.Text small type={stepNumber < 1 ? 'secondary' : 'default'}>
|
||||
<a>{project ? project.name : 'Create a new project'}</a>
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<IconChevronRight size="small" />
|
||||
</Typography.Text>
|
||||
<Typography.Text small type={stepNumber < 2 ? 'secondary' : 'default'}>
|
||||
<a>{project ? project.name : 'Extend your database'}</a>
|
||||
</Typography.Text>
|
||||
<IconChevronRight size="small" className="text-scale-1100" />
|
||||
<p className="text-sm">
|
||||
{organization ? `Organization: ${organization.name}` : 'Create an organization'}
|
||||
</p>
|
||||
<IconChevronRight size="small" className="text-scale-1100" />
|
||||
<p className={`text-sm ${stepNumber < 1 ? 'text-scale-1100' : ''}`}>
|
||||
{project ? project.name : 'Create a new project'}
|
||||
</p>
|
||||
<IconChevronRight size="small" className="text-scale-1100" />
|
||||
<p className={`text-sm ${stepNumber < 2 ? 'text-scale-1100' : ''}`}>
|
||||
{project ? project.name : 'Extend your database'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">{/* End */}</div>
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import { Typography, IconAlertCircle } from '@supabase/ui'
|
||||
import { Typography, IconAlertCircle, Alert } from '@supabase/ui'
|
||||
|
||||
const AlphaPreview = () => {
|
||||
return (
|
||||
<div className="block w-full bg-yellow-500 bg-opacity-5 p-3 border border-yellow-500 border-opacity-50 rounded">
|
||||
<div className="flex space-x-3">
|
||||
<div>
|
||||
<IconAlertCircle className="text-yellow-500" size="large" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Typography.Text type="warning">Alpha preview</Typography.Text>
|
||||
<div>
|
||||
<Typography.Text type="warning" small>
|
||||
This is not suitable for production
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Alert title="Alpha preview" withIcon variant="warning">
|
||||
This is not suitable for production
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Typography, Input, Radio } from '@supabase/ui'
|
||||
import { Button, Typography, Input, Radio, Modal } from '@supabase/ui'
|
||||
import { get } from 'lodash'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
@@ -9,19 +9,20 @@ const PolicyName = ({ name = '', onUpdatePolicyName = () => {} }) => {
|
||||
return (
|
||||
<div className="flex space-x-12">
|
||||
<div className="w-1/3 flex flex-col space-y-2">
|
||||
<Typography.Text>Policy name</Typography.Text>
|
||||
<Typography.Text type="secondary">A descriptive name for your policy.</Typography.Text>
|
||||
<label className="text-base text-scale-1100" htmlFor="policy-name">
|
||||
Policy name
|
||||
</label>
|
||||
<p className="text-sm text-scale-900">A descriptive name for your policy</p>
|
||||
</div>
|
||||
<div className="w-2/3 relative">
|
||||
<Input
|
||||
id="policy-name"
|
||||
value={name}
|
||||
rows={1}
|
||||
limit={63}
|
||||
onChange={(e) => onUpdatePolicyName(e.target.value)}
|
||||
actions={<span className="text-sm text-scale-900 mr-3">{name.length}/63</span>}
|
||||
/>
|
||||
<div className="absolute top-2 right-4">
|
||||
<Typography.Text>{name.length}/63</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -49,10 +50,12 @@ const PolicyDefinition = ({
|
||||
{showUsing(operation) && (
|
||||
<div className="flex space-x-12">
|
||||
<div className="w-1/3 flex flex-col space-y-2">
|
||||
<Typography.Text>USING expression</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<label className="text-base text-scale-1100" htmlFor="policy-name">
|
||||
USING expression
|
||||
</label>
|
||||
<p className="text-sm text-scale-900">
|
||||
Provide a SQL conditional expression that returns a boolean.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`w-2/3 ${showCheck(operation) ? 'h-32' : 'h-56'}`}>
|
||||
<SqlEditor defaultValue={definition} onInputChange={onUpdatePolicyUsing} />
|
||||
@@ -62,10 +65,12 @@ const PolicyDefinition = ({
|
||||
{showCheck(operation) && (
|
||||
<div className="flex space-x-12">
|
||||
<div className="w-1/3 flex flex-col space-y-2">
|
||||
<Typography.Text>WITH CHECK expression</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<label className="text-base text-scale-1100" htmlFor="policy-name">
|
||||
WITH CHECK expression
|
||||
</label>
|
||||
<p className="text-sm text-scale-900">
|
||||
Provide a SQL conditional expression that returns a boolean.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`w-2/3 ${showUsing(operation) ? 'h-32' : 'h-56'}`}>
|
||||
<SqlEditor defaultValue={check} onInputChange={onUpdatePolicyCheck} />
|
||||
@@ -80,46 +85,50 @@ const PolicyAllowedOperation = ({ operation = '', onSelectOperation = () => {} }
|
||||
return (
|
||||
<div className="flex justify-between space-x-12">
|
||||
<div className="w-1/3 flex flex-col space-y-2">
|
||||
<Typography.Text>Allowed operation</Typography.Text>
|
||||
<Typography.Text type="secondary">Selection an operation for this policy</Typography.Text>
|
||||
<label className="text-base text-scale-1100" htmlFor="allowed-operation">
|
||||
Allowed operation
|
||||
</label>
|
||||
<p className="text-sm text-scale-900">Selection an operation for this policy</p>
|
||||
</div>
|
||||
<div className="w-2/3">
|
||||
<div className="flex items-center space-x-8">
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="SELECT"
|
||||
value="SELECT"
|
||||
checked={operation === 'SELECT'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="INSERT"
|
||||
value="INSERT"
|
||||
checked={operation === 'INSERT'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="UPDATE"
|
||||
value="UPDATE"
|
||||
checked={operation === 'UPDATE'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="DELETE"
|
||||
value="DELETE"
|
||||
checked={operation === 'DELETE'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="ALL"
|
||||
value="ALL"
|
||||
checked={operation === 'ALL'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio.Group type="small-cards" id="allowed-operation">
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="SELECT"
|
||||
value="SELECT"
|
||||
checked={operation === 'SELECT'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="INSERT"
|
||||
value="INSERT"
|
||||
checked={operation === 'INSERT'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="UPDATE"
|
||||
value="UPDATE"
|
||||
checked={operation === 'UPDATE'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="DELETE"
|
||||
value="DELETE"
|
||||
checked={operation === 'DELETE'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
<Radio
|
||||
name="allowed-operation"
|
||||
label="ALL"
|
||||
value="ALL"
|
||||
checked={operation === 'ALL'}
|
||||
onChange={(e) => onSelectOperation(e.target.value)}
|
||||
/>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,9 +140,9 @@ const PolicyEditorFooter = ({
|
||||
onViewTemplates = () => {},
|
||||
onReviewPolicy = () => {},
|
||||
}) => (
|
||||
<div className="px-6 py-3 w-full flex justify-end items-center space-x-4 border-t dark:border-dark">
|
||||
<div className="px-6 py-4 w-full flex justify-end items-center gap-2 border-t dark:border-dark">
|
||||
{showTemplates && (
|
||||
<Button type="secondary" onClick={onViewTemplates}>
|
||||
<Button type="default" onClick={onViewTemplates}>
|
||||
View templates
|
||||
</Button>
|
||||
)}
|
||||
@@ -156,24 +165,34 @@ const PolicyEditor = ({
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="px-6 space-y-8 mb-8">
|
||||
<PolicyName
|
||||
name={policyFormFields.name}
|
||||
onUpdatePolicyName={(name) => onUpdatePolicyFormFields({ name })}
|
||||
/>
|
||||
{isNewPolicy && (
|
||||
<PolicyAllowedOperation
|
||||
operation={operation}
|
||||
onSelectOperation={(command) => onUpdatePolicyFormFields({ command })}
|
||||
<div className="py-8 space-y-8 mb-8">
|
||||
<Modal.Content>
|
||||
<PolicyName
|
||||
name={policyFormFields.name}
|
||||
onUpdatePolicyName={(name) => onUpdatePolicyFormFields({ name })}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Seperator />
|
||||
{isNewPolicy && (
|
||||
<>
|
||||
<Modal.Content>
|
||||
<PolicyAllowedOperation
|
||||
operation={operation}
|
||||
onSelectOperation={(command) => onUpdatePolicyFormFields({ command })}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Seperator />
|
||||
</>
|
||||
)}
|
||||
<PolicyDefinition
|
||||
operation={operation}
|
||||
definition={definition}
|
||||
check={check}
|
||||
onUpdatePolicyUsing={(definition) => onUpdatePolicyFormFields({ definition })}
|
||||
onUpdatePolicyCheck={(check) => onUpdatePolicyFormFields({ check })}
|
||||
/>
|
||||
<Modal.Content>
|
||||
<PolicyDefinition
|
||||
operation={operation}
|
||||
definition={definition}
|
||||
check={check}
|
||||
onUpdatePolicyUsing={(definition) => onUpdatePolicyFormFields({ definition })}
|
||||
onUpdatePolicyCheck={(check) => onUpdatePolicyFormFields({ check })}
|
||||
/>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
<PolicyEditorFooter
|
||||
showTemplates={isNewPolicy}
|
||||
|
||||
@@ -33,13 +33,14 @@ const PolicyEditorModalTitle = ({
|
||||
}
|
||||
if (view === POLICY_MODAL_VIEWS.TEMPLATES) {
|
||||
return (
|
||||
<div className="px-6 pt-5">
|
||||
<div className="">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Typography.Text type="secondary">
|
||||
<div className="cursor-pointer" onClick={onSelectBackFromTemplates}>
|
||||
<IconChevronLeft />
|
||||
</div>
|
||||
</Typography.Text>
|
||||
<span
|
||||
onClick={onSelectBackFromTemplates}
|
||||
className="cursor-pointer text-scale-900 hover:text-scale-1200 transition-colors"
|
||||
>
|
||||
<IconChevronLeft strokeWidth={2} size={14} />
|
||||
</span>
|
||||
<Typography.Title level={4} className="m-0">
|
||||
Select a template to use for your new policy
|
||||
</Typography.Title>
|
||||
@@ -48,7 +49,7 @@ const PolicyEditorModalTitle = ({
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center space-x-3 px-6 pt-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Typography.Title level={4} className="m-0">
|
||||
{getTitle()}
|
||||
</Typography.Title>
|
||||
@@ -174,11 +175,12 @@ const PolicyEditorModal = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={view === POLICY_MODAL_VIEWS.SELECTION ? 'medium' : 'xxlarge'}
|
||||
closable
|
||||
hideFooter
|
||||
visible={visible}
|
||||
contentStyle={{ padding: 0 }}
|
||||
title={[
|
||||
header={[
|
||||
<PolicyEditorModalTitle
|
||||
key="0"
|
||||
view={view}
|
||||
@@ -188,10 +190,10 @@ const PolicyEditorModal = ({
|
||||
onSelectBackFromTemplates={onSelectBackFromTemplates}
|
||||
/>,
|
||||
]}
|
||||
style={{ maxWidth: 'none', width: '60rem' }}
|
||||
// style={{ maxWidth: 'none', width: '60rem' }}
|
||||
onCancel={onSelectCancel}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="">
|
||||
{view === POLICY_MODAL_VIEWS.SELECTION ? (
|
||||
<PolicySelection
|
||||
description="Write rules with PostgreSQL's policies to fit your unique business needs."
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
import { Button, Modal } from '@supabase/ui'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
|
||||
import SqlEditor from '../SqlEditor'
|
||||
|
||||
const ReviewEmptyState = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center my-10 opacity-50 space-x-2">
|
||||
<Typography.Text>There are no changes made to this policy</Typography.Text>
|
||||
<p className="text-base text-scale-1100">There are no changes made to this policy</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,35 +22,39 @@ const PolicyReview = ({ policy = {}, onSelectBack = () => {}, onSelectSave = ()
|
||||
let formattedSQLStatement = policy.statement || ''
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="px-6 space-y-8 flex items-center justify-between space-x-4">
|
||||
<div className="flex flex-col">
|
||||
<Typography.Text>
|
||||
This is the SQL statement that will be used to create your policy.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 space-y-4 overflow-y-auto" style={{ maxHeight: '25rem' }}>
|
||||
{isEmpty(policy) ? (
|
||||
<ReviewEmptyState />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Typography.Text>{policy.description}</Typography.Text>
|
||||
<div className="h-40">
|
||||
<SqlEditor readOnly defaultValue={formattedSQLStatement} />
|
||||
<>
|
||||
<Modal.Content>
|
||||
<div className="space-y-6 py-8">
|
||||
<div className="space-y-8 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-scale-1100">
|
||||
This is the SQL statement that will be used to create your policy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 py-3 w-full flex justify-end items-center space-x-4 border-t dark:border-dark">
|
||||
<Button type="secondary" onClick={onSelectBack}>
|
||||
<div className="space-y-4 overflow-y-auto" style={{ maxHeight: '25rem' }}>
|
||||
{isEmpty(policy) ? (
|
||||
<ReviewEmptyState />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<span>{policy.description}</span>
|
||||
<div className="h-40">
|
||||
<SqlEditor readOnly defaultValue={formattedSQLStatement} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<div className="px-6 py-4 w-full flex justify-end items-center gap-2 border-t dark:border-dark">
|
||||
<Button type="default" onClick={onSelectBack}>
|
||||
Back to edit
|
||||
</Button>
|
||||
<Button type="primary" disabled={isEmpty(policy)} onClick={onSavePolicy} loading={isSaving}>
|
||||
Save policy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, Typography } from '@supabase/ui'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { IconEdit, IconGrid, Modal } from '@supabase/ui'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
|
||||
const PolicySelection = ({
|
||||
description = '',
|
||||
@@ -7,64 +7,68 @@ const PolicySelection = ({
|
||||
onViewEditor = () => {},
|
||||
}) => {
|
||||
return (
|
||||
<div className="px-6">
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<div className="flex flex-col w-3/4">
|
||||
<Typography.Text>{description}</Typography.Text>
|
||||
<Modal.Content>
|
||||
<div className="py-8 space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<div className="flex items-center justify-center space-x-8">
|
||||
<div onClick={onViewTemplates}>
|
||||
<Card hoverable className="cursor-pointer py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-1/2">
|
||||
<Typography.Text>Get started quickly</Typography.Text>
|
||||
<Typography.Title level={3}>Create a policy from a template</Typography.Title>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-1 gap-2">
|
||||
<CardButton
|
||||
hoverable
|
||||
className="cursor-pointer py-4"
|
||||
onClick={onViewTemplates}
|
||||
// imgUrl={'/img/policy-template.svg'}
|
||||
title="Get started quickly"
|
||||
description="Create a policy from a template"
|
||||
icon={
|
||||
<div className="flex">
|
||||
<div
|
||||
className="
|
||||
w-8 h-8 bg-scale-1200 text-scale-100
|
||||
rounded
|
||||
flex items-center justify-center
|
||||
"
|
||||
>
|
||||
<IconGrid size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<SVG
|
||||
src={'/img/policy-template.svg'}
|
||||
alt={'policy-template'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="w-24 h-24 text-color-inherit opacity-75"')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div onClick={onViewEditor}>
|
||||
<Card hoverable className="cursor-pointer py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-1/2">
|
||||
<Typography.Text>For full customization</Typography.Text>
|
||||
<Typography.Title level={3}>Create a policy from scratch</Typography.Title>
|
||||
}
|
||||
/>
|
||||
|
||||
<CardButton
|
||||
hoverable
|
||||
className="cursor-pointer py-4"
|
||||
onClick={onViewEditor}
|
||||
title="For full customization"
|
||||
description="Create a policy from scratch"
|
||||
icon={
|
||||
<div className="flex">
|
||||
<div
|
||||
className="
|
||||
w-8 h-8 bg-scale-1200 text-scale-100
|
||||
rounded
|
||||
flex items-center justify-center
|
||||
"
|
||||
>
|
||||
<IconEdit size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<SVG
|
||||
src={'/img/policy-new.svg'}
|
||||
alt={'policy-new'}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="w-24 h-24 text-color-inherit opacity-75"')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 mx-auto text-center">
|
||||
<Typography.Text>
|
||||
Not sure what policies are? Check out our resources{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
className="text-green-400"
|
||||
href="https://supabase.com/docs/guides/auth#policies"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Typography.Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Not sure what policies are? Check out our resources{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
className="text-brand-900 hover:text-brand-1200 transition-colors"
|
||||
href="https://supabase.com/docs/guides/auth#policies"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Badge, Button, Typography } from '@supabase/ui'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
IconEdit,
|
||||
IconMoreVertical,
|
||||
IconTrash,
|
||||
Typography,
|
||||
} from '@supabase/ui'
|
||||
import Panel from '../Panel'
|
||||
|
||||
const PolicyTableRowHeader = ({
|
||||
@@ -36,29 +44,49 @@ const PolicyRow = ({
|
||||
onSelectDeletePolicy = () => {},
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex has-hidden-children">
|
||||
<Panel.Content className="flex justify-between w-full py-4 border-b dark:border-dark">
|
||||
<div className="flex-1">
|
||||
<Typography.Text>{policy.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" className="font-mono block" small>
|
||||
{policy.definition || policy.check}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="relative w-64">
|
||||
<div className="top-0 right-0 absolute inline-block visible-child my-2">
|
||||
<Typography.Text type="secondary" className='font-mono text-right' small>
|
||||
{policy.command}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="top-0 right-0 absolute inline-block -mr-3 hidden-child">
|
||||
<Button type="outline" className="mx-2" onClick={() => onSelectEditPolicy(policy)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button type="outline" onClick={() => onSelectDeletePolicy(policy)}>
|
||||
Delete
|
||||
</Button>
|
||||
<div className="group">
|
||||
<Panel.Content className="flex gap-2 justify-between py-4 border-b border-panel-border-light dark:border-panel-border-dark">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-3">
|
||||
<div className="font-mono text-xs text-scale-900">{policy.command}</div>
|
||||
<div className="flex flex-col lg:flex-row gap-2">
|
||||
<span className="text-sm text-scale-1200 truncate">{policy.name}</span>
|
||||
<span className="text-sm text-scale-1100 truncate">
|
||||
{policy.definition || policy.check}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
size="small"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Item
|
||||
icon={<IconEdit size={14} />}
|
||||
type="outline"
|
||||
className="mx-2"
|
||||
onClick={() => onSelectEditPolicy(policy)}
|
||||
>
|
||||
Edit
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item
|
||||
icon={<IconTrash size={14} />}
|
||||
type="outline"
|
||||
onClick={() => onSelectDeletePolicy(policy)}
|
||||
>
|
||||
Delete
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
style={{ paddingLeft: 4, paddingRight: 4 }}
|
||||
icon={<IconMoreVertical />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Panel.Content>
|
||||
</div>
|
||||
)
|
||||
@@ -83,9 +111,7 @@ const PolicyTableRow = ({
|
||||
>
|
||||
{table.policies.length === 0 && (
|
||||
<div className="p-4 px-6">
|
||||
<Typography.Text type="secondary" className="opacity-50">
|
||||
No policies created yet
|
||||
</Typography.Text>
|
||||
<p className="text-sm text-scale-900">No policies created yet</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Typography, Menu } from '@supabase/ui'
|
||||
import { Button, Menu } from '@supabase/ui'
|
||||
import { useState } from 'react'
|
||||
import { isEmpty } from 'lodash'
|
||||
import SqlEditor from '../SqlEditor'
|
||||
@@ -14,29 +14,31 @@ const TemplatesList = ({
|
||||
className="space-y-0 overflow-y-auto hide-scrollbar divide-y divide-solid divide-border-primary"
|
||||
style={{ maxHeight: '24rem' }}
|
||||
>
|
||||
<Menu className="dark:divide-gray-600">
|
||||
{templates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
onClick={() => setSelectedTemplate(template)}
|
||||
className="divide-gray-900"
|
||||
>
|
||||
<Menu.Item
|
||||
className="cursor-pointer"
|
||||
showActiveBar
|
||||
active={selectedTemplate === template}
|
||||
<Menu type="border">
|
||||
{templates.map((template) => {
|
||||
const active = selectedTemplate === template
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'hover:bg-scale-400 border-b border-scale-400 ' +
|
||||
(active ? 'bg-scale-300 dark:bg-scale-500' : '')
|
||||
}
|
||||
>
|
||||
<div className="space-y-2 py-2">
|
||||
<Typography.Text>{template.templateName}</Typography.Text>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
))}
|
||||
<Menu.Item
|
||||
key={template.id}
|
||||
active={active}
|
||||
onClick={() => setSelectedTemplate(template)}
|
||||
>
|
||||
<div className="py-2 truncate">{template.templateName}</div>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
{templatesNote && (
|
||||
<div className="px-4 py-2">
|
||||
<Typography.Text small>{templatesNote}</Typography.Text>
|
||||
<p className="text-xs text-scale-900">{templatesNote}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -51,12 +53,12 @@ const TemplatePreview = ({ selectedTemplate = {} }) => {
|
||||
<div className="px-6 my-5 space-y-6 h-full">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Typography.Title level={6}>{templateName}</Typography.Title>
|
||||
<Typography.Text>{description}</Typography.Text>
|
||||
<h3 className="text-base text-scale-1200">{templateName}</h3>
|
||||
<p className="text-sm text-scale-1100">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Typography.Text>Policy SQL template:</Typography.Text>
|
||||
<label className="text-sm text-scale-1100">Policy SQL template:</label>
|
||||
<div className="h-64">
|
||||
<SqlEditor readOnly queryId={id} defaultValue={statement} />
|
||||
</div>
|
||||
@@ -81,10 +83,10 @@ const PolicyTemplates = ({ templates = [], templatesNote = '', onUseTemplate = (
|
||||
/>
|
||||
<TemplatePreview selectedTemplate={selectedTemplate} />
|
||||
</div>
|
||||
<div className="px-6 py-2 w-full flex justify-end items-center space-x-4 border-t dark:border-dark">
|
||||
<Typography.Text type="secondary">
|
||||
<div className="px-6 py-4 w-full flex justify-end items-center gap-3 border-t dark:border-dark">
|
||||
<span className="text-sm text-scale-900">
|
||||
This will override any existing code you've written
|
||||
</Typography.Text>
|
||||
</span>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isEmpty(selectedTemplate)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Badge, Typography, IconArrowRight, IconLoader } from '@supabase/ui'
|
||||
import { Badge, IconArrowRight, IconLoader, Button } from '@supabase/ui'
|
||||
import {
|
||||
DisplayApiSettings,
|
||||
DisplayConfigSettings,
|
||||
@@ -15,8 +15,6 @@ import { API_URL } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
type ProjectBuildingState = {} & any
|
||||
const ProjectBuildingState: React.FC<ProjectBuildingState> = () => {
|
||||
const { ui, app } = useStore()
|
||||
@@ -49,51 +47,69 @@ const ProjectBuildingState: React.FC<ProjectBuildingState> = () => {
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto w-full items-center justify-center my-16">
|
||||
<div className="flex flex-col space-y-16">
|
||||
<div className="mx-6">
|
||||
<div className="flex flex-col space-y-16 mx-6">
|
||||
<div className=" flex flex-col gap-4">
|
||||
<div className="space-x-3 flex items-center">
|
||||
<Title level={3} className="mb-0">
|
||||
Setting up {project.name}
|
||||
</Title>
|
||||
<IconLoader className="animate-spin" />
|
||||
<h1 className="text-scale-1200 text-3xl">{project.name}</h1>
|
||||
<Badge color="brand">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconLoader className="animate-spin" size={12} />
|
||||
<span>Setting up project</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
<Text type="secondary">Provisioning your database and API endpoints</Text>
|
||||
<div className="mt-3">
|
||||
<Badge color="yellow">This may take a few minutes</Badge>
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
{' '}
|
||||
We are provisioning your database and API endpoints
|
||||
</p>
|
||||
<p className="text-scale-1100 text-sm"> This may take a few minutes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mx-6 grid grid-cols-12 gap-12">
|
||||
<div className=" grid grid-cols-12 gap-12">
|
||||
<div className="col-span-12 lg:col-span-4 space-y-12">
|
||||
<div>
|
||||
<Title level={5}>While you wait</Title>
|
||||
<h4 className="text-scale-1200 text-base">While you wait</h4>
|
||||
|
||||
<ChecklistItem
|
||||
description={
|
||||
<Text>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Browse the Supabase{' '}
|
||||
<Link href="https://supabase.com/docs">
|
||||
<a className="text-green-500 hover:text-green-700" target="_blank">
|
||||
<a
|
||||
className="text-brand-900 hover:text-brand-1200 transition-colors mb-0"
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Title level={5}>Not working?</Title>
|
||||
<h4 className="text-scale-1200 text-base">Not working?</h4>
|
||||
<ChecklistItem
|
||||
description={<Text>Try refreshing after a couple of minutes.</Text>}
|
||||
description={
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Try refreshing after a couple of minutes.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<ul>
|
||||
<ChecklistItem
|
||||
description={
|
||||
<Typography.Text>
|
||||
If your dashboard hasn't connected within 2 minutes, send us an email:{' '}
|
||||
<Typography.Text code>support@supabase.io</Typography.Text>
|
||||
</Typography.Text>
|
||||
<>
|
||||
<p className="text-scale-1100 text-sm mb-4">
|
||||
If your dashboard hasn't connected within 2 minutes, you can open a
|
||||
support ticket.
|
||||
</p>
|
||||
<Link href="/support/new">
|
||||
<Button type="default">Contact support team</Button>
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
@@ -109,7 +125,7 @@ const ProjectBuildingState: React.FC<ProjectBuildingState> = () => {
|
||||
<div className="max-w-7xl mx-auto w-full my-16 space-y-16">
|
||||
<div className="space-y-8">
|
||||
<div className="mx-6">
|
||||
<Typography.Title level={4}>Client libraries</Typography.Title>
|
||||
<h5>Client libraries</h5>
|
||||
</div>
|
||||
<div className="mx-6 grid md:grid-cols-3 gap-12 mb-12">
|
||||
{CLIENT_LIBRARIES.map((library) => (
|
||||
@@ -119,7 +135,7 @@ const ProjectBuildingState: React.FC<ProjectBuildingState> = () => {
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="mx-6">
|
||||
<Typography.Title level={4}>Example projects</Typography.Title>
|
||||
<h5>Example projects</h5>
|
||||
</div>
|
||||
<div className="mx-6 grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{EXAMPLE_PROJECTS.map((project) => (
|
||||
@@ -136,8 +152,8 @@ export default observer(ProjectBuildingState)
|
||||
const ChecklistItem = ({ description }: any) => {
|
||||
return (
|
||||
<li className="flex flex-wrap my-3 space-x-3">
|
||||
<div className="mt-1.5">
|
||||
<IconArrowRight className="text-green-500" size="tiny" />
|
||||
<div className="mt-0.5">
|
||||
<IconArrowRight className="text-scale-900" size="tiny" />
|
||||
</div>
|
||||
<div className="flex-1">{description}</div>
|
||||
</li>
|
||||
|
||||
@@ -124,12 +124,8 @@ const ChartHandler: FC<Props> = ({
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-52 flex flex-col space-y-4 items-center justify-center">
|
||||
<Typography.Text className="animate-spin">
|
||||
<IconLoader />
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" small>
|
||||
Loading data for {label}
|
||||
</Typography.Text>
|
||||
<IconLoader className="animate-spin text-scale-700" />
|
||||
<p className="text-xs text-scale-900">Loading data for {label}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -137,12 +133,8 @@ const ChartHandler: FC<Props> = ({
|
||||
if (isUndefined(chartData)) {
|
||||
return (
|
||||
<div className="w-full h-52 flex flex-col space-y-4 items-center justify-center">
|
||||
<Typography.Text>
|
||||
<IconAlertCircle />
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" small>
|
||||
Unable to load data for {label}
|
||||
</Typography.Text>
|
||||
<IconAlertCircle className="text-scale-700" />
|
||||
<p className="text-scale-900 text-xs">Unable to load data for {label}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconBarChart2, Loading, Typography } from '@supabase/ui'
|
||||
import { IconBarChart2, Loading } from '@supabase/ui'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
BarChart as RechartBarChart,
|
||||
@@ -88,24 +88,28 @@ const Header = ({
|
||||
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
|
||||
|
||||
const chartTitle = (
|
||||
<Typography.Text small={minimalHeader ? true : false} className="mb-0" type="secondary">
|
||||
<h3 className={'text-scale-900 ' + (minimalHeader ? 'text-xs' : 'text-sm')}>
|
||||
{label ?? attribute}
|
||||
</Typography.Text>
|
||||
</h3>
|
||||
)
|
||||
const highlighted = (
|
||||
<Typography.Title level={minimalHeader ? 5 : 3} className="my-0 font-normal">
|
||||
<h5
|
||||
className={
|
||||
'text-xl text-scale-1200 font-normal ' + (minimalHeader ? 'text-base' : 'text-2xl')
|
||||
}
|
||||
>
|
||||
{title}
|
||||
<span className="text-lg">{format}</span>
|
||||
</Typography.Title>
|
||||
</h5>
|
||||
)
|
||||
const date = (
|
||||
<Typography.Text type="secondary" className="opacity-50" small>
|
||||
<h5 className="text-xs text-scale-900">
|
||||
{focus ? (
|
||||
data && data[focus] && day(data[focus].period_start).format(FOCUS_FORMAT)
|
||||
) : (
|
||||
<span className="opacity-0">x</span>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</h5>
|
||||
)
|
||||
|
||||
if (minimalHeader) {
|
||||
@@ -133,16 +137,32 @@ const NoData = () => (
|
||||
<div
|
||||
className="
|
||||
h-full w-full
|
||||
border border-dashed dark:border-dark
|
||||
border border-dashed border-scale-600
|
||||
flex flex-col items-center justify-center
|
||||
space-y-2 text-center
|
||||
"
|
||||
>
|
||||
<Typography.Text className="mb-2">
|
||||
<IconBarChart2 />
|
||||
</Typography.Text>
|
||||
<Typography.Text>No data to show</Typography.Text>
|
||||
<IconBarChart2 className="text-scale-800" />
|
||||
<div>
|
||||
<p className="text-scale-1100 text-xs">No data to show</p>
|
||||
<p className="text-scale-900 text-xs">May take 24 hours for data to show</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
const total = (data: any, format: any, attribute: any) => {
|
||||
let total = 0
|
||||
data?.map((item: any) => {
|
||||
total = total + Number(item[attribute])
|
||||
})
|
||||
if (format === '%') {
|
||||
return Number(total).toFixed(2)
|
||||
}
|
||||
return numberWithCommas(total)
|
||||
}
|
||||
|
||||
function numberWithCommas(x: any) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
export function BarChart({
|
||||
data,
|
||||
@@ -253,17 +273,17 @@ export function BarChart({
|
||||
</RechartBarChart>
|
||||
</ResponsiveContainer>
|
||||
{data && (
|
||||
<div className="flex items-center justify-between -mt-5">
|
||||
<Typography.Text type="secondary" className="opacity-50" small>
|
||||
<div className="text-xs text-scale-900 flex items-center justify-between -mt-5">
|
||||
<span>
|
||||
{day(data[0].period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" className="opacity-50" small>
|
||||
</span>
|
||||
<span>
|
||||
{day(data[data?.length - 1]?.period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -372,17 +392,17 @@ export function AreaChart({
|
||||
</RechartAreaChart>
|
||||
</ResponsiveContainer>
|
||||
{data && (
|
||||
<div className="flex items-center justify-between -mt-5">
|
||||
<Typography.Text type="secondary" className="opacity-50" small>
|
||||
<div className="text-scale-900 text-xs flex items-center justify-between -mt-5">
|
||||
<span>
|
||||
{dayjs(data[0].period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" className="opacity-50" small>
|
||||
</span>
|
||||
<span>
|
||||
{dayjs(data[data?.length - 1]?.period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,8 +2,8 @@ import useSWR from 'swr'
|
||||
import { FC } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { Badge, IconAlertCircle, Input, Loading, Typography } from '@supabase/ui'
|
||||
import { JwtSecretUpdateStatus, ProjectEvents } from '@supabase/shared-types/out/events'
|
||||
import { IconAlertCircle, Input, Loading, Typography } from '@supabase/ui'
|
||||
|
||||
import { useJwtSecretUpdateStatus } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
@@ -71,15 +71,17 @@ export const DisplayApiSettings = () => {
|
||||
label={
|
||||
<>
|
||||
{x.tags?.split(',').map((x: any, i: number) => (
|
||||
<code key={`${x}${i}`} className="text-xs bg-gray-500 text-white px-2">
|
||||
<code key={`${x}${i}`} className="text-xs text-white px-2">
|
||||
{x}
|
||||
</code>
|
||||
))}
|
||||
{x.tags === 'service_role' && (
|
||||
<code className="text-xs bg-red-500 px-2 ml-1 text-white">{'secret'}</code>
|
||||
<>
|
||||
<code className="text-xs bg-red-900 px-2 ml-1 text-white">{'secret'}</code>
|
||||
</>
|
||||
)}
|
||||
{x.tags === 'anon' && (
|
||||
<code className="text-xs bg-gray-500 text-white px-2 ml-1">{'public'}</code>
|
||||
<code className="text-xs text-white px-2 ml-1">{'public'}</code>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -201,12 +203,12 @@ const ApiContentWrapper: FC<any> = ({ children }) => {
|
||||
<Panel
|
||||
title={
|
||||
<div className="space-y-3">
|
||||
<Title level={5}>Project API keys</Title>
|
||||
<Text className="block" type="secondary">
|
||||
<h5 className="text-base">Project API keys</h5>
|
||||
<p className="text-sm text-scale-1000">
|
||||
Your API is secured behind an API gateway which requires an API Key for every request.
|
||||
<br />
|
||||
You can use the keys below to use Supabase client libraries.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -220,7 +222,7 @@ const ConfigContentWrapper: FC<any> = ({ children }) => {
|
||||
<Panel
|
||||
title={
|
||||
<div className="space-y-3">
|
||||
<Title level={5}>Project Configuration</Title>
|
||||
<h5 className="text-base">Project Configuration</h5>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
Loaded 100 of 201 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user