Merge branch 'master' into hi/improve-loading-time

This commit is contained in:
phamhieu committed 2022-03-01 19:00:05 +07:00
commit 2692310024
201 files changed
+7197 -4904

No files matched your search

+3 -3
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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">&nbsp;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>
+21 -24
View File
@@ -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>
@@ -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>
@@ -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>
)
}
@@ -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>
@@ -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>
))}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>}
</>
)
}
@@ -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
@@ -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>
)
}
@@ -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>
}
/>
</>
)
@@ -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 && (
@@ -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>
)}
@@ -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>
)
}
@@ -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
@@ -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>
)
}
@@ -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' : ''
}`}
>
@@ -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>
)
}
@@ -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.&#10;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.&#10;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>
+12 -22
View File
@@ -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