) : isCompleted ? (
-
+
) : (
-
+
)}
{
{initiated_at !== undefined && (
-
+
Started on: {initiatedAtUTC} (UTC)
diff --git a/studio/components/ui/ProjectUpgradeFailedBanner.tsx b/studio/components/ui/ProjectUpgradeFailedBanner.tsx
index 5e23e6634e2..19bab87189a 100644
--- a/studio/components/ui/ProjectUpgradeFailedBanner.tsx
+++ b/studio/components/ui/ProjectUpgradeFailedBanner.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { useState } from 'react'
import { Alert, Button, IconX } from 'ui'
import { useParams } from 'hooks'
+import { IS_PLATFORM } from 'lib/constants'
import { useProjectUpgradingStatusQuery } from 'data/config/project-upgrade-status-query'
import { DatabaseUpgradeStatus } from '@supabase/shared-types/out/events'
@@ -10,7 +11,7 @@ import { DatabaseUpgradeStatus } from '@supabase/shared-types/out/events'
const ProjectUpgradeFailedBanner = () => {
const { ref } = useParams()
- const { data } = useProjectUpgradingStatusQuery({ projectRef: ref })
+ const { data } = useProjectUpgradingStatusQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const { target_version, status, initiated_at, error } = data?.databaseUpgradeStatus ?? {}
const key = `supabase-upgrade-${ref}-${initiated_at}`
@@ -39,7 +40,7 @@ const ProjectUpgradeFailedBanner = () => {
variant={'warning'}
title={`Postgres version upgrade to ${target_version} was not successful (Initiated at ${initiatedAtUTC} UTC)`}
actions={
-
+
diff --git a/studio/localStores/storageExplorer/StorageExplorerStore.js b/studio/localStores/storageExplorer/StorageExplorerStore.js
index 57533c66f99..d35a985f3d4 100644
--- a/studio/localStores/storageExplorer/StorageExplorerStore.js
+++ b/studio/localStores/storageExplorer/StorageExplorerStore.js
@@ -1256,7 +1256,7 @@ class StorageExplorerStore {
path: prefix,
options,
})
- if (res.error) console.error('Error at fetchFoldersByPath:', error)
+ if (res.error) console.error('Error at fetchFoldersByPath:', res.error)
return res
})
)
diff --git a/studio/pages/api/projects/[ref]/api/rest.ts b/studio/pages/api/projects/[ref]/api/rest.ts
new file mode 100644
index 00000000000..d4c2a519ac3
--- /dev/null
+++ b/studio/pages/api/projects/[ref]/api/rest.ts
@@ -0,0 +1,38 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import apiWrapper from 'lib/api/apiWrapper'
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'GET':
+ return handleGet(req, res)
+ case 'HEAD':
+ return handleHead(req, res)
+ default:
+ res.setHeader('Allow', ['GET', 'HEAD'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
+ const response = await fetch(`${process.env.SUPABASE_URL}/rest/v1/`, {
+ method: 'GET',
+ headers: {
+ apikey: process.env.SUPABASE_SERVICE_KEY!,
+ },
+ })
+ if (response.ok) {
+ const data = await response.json()
+
+ return res.status(200).json(data)
+ }
+
+ return res.status(500).json({ error: { message: 'Internal Server Error' } })
+}
+
+const handleHead = async (req: NextApiRequest, res: NextApiResponse) => {
+ res.status(200).end()
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/empty.ts b/studio/pages/api/storage/[ref]/buckets/[id]/empty.ts
new file mode 100644
index 00000000000..9b162f6a637
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/empty.ts
@@ -0,0 +1,30 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'POST':
+ return handlePost(req, res)
+ default:
+ res.setHeader('Allow', ['POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+
+ const { data, error } = await supabase.storage.emptyBucket(id as string)
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/index.ts b/studio/pages/api/storage/[ref]/buckets/[id]/index.ts
new file mode 100644
index 00000000000..72af258757b
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/index.ts
@@ -0,0 +1,46 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'PATCH':
+ return handlePatch(req, res)
+ case 'DELETE':
+ return handleDelete(req, res)
+ default:
+ res.setHeader('Allow', ['PATCH', 'DELETE'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handlePatch = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+ const { public: isPublicBucket } = req.body
+
+ const { data, error } = await supabase.storage.updateBucket(id as string, {
+ public: isPublicBucket,
+ })
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res.status(200).json(data)
+}
+
+const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+
+ const { data, error } = await supabase.storage.deleteBucket(id as string)
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/download.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/download.ts
new file mode 100644
index 00000000000..fa51a404ed0
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/download.ts
@@ -0,0 +1,34 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'POST':
+ return handlePost(req, res)
+ default:
+ res.setHeader('Allow', ['POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+ const { path } = req.body
+
+ const { data, error } = await supabase.storage.from(id as string).download(path)
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res
+ .status(200)
+ .setHeader('Content-Type', 'application/octet-stream')
+ .send(Buffer.from(await data.arrayBuffer()))
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/index.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/index.ts
new file mode 100644
index 00000000000..d80425dd173
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/index.ts
@@ -0,0 +1,31 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'DELETE':
+ return handleDelete(req, res)
+ default:
+ res.setHeader('Allow', ['DELETE'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+ const { paths } = req.body
+
+ const { data, error } = await supabase.storage.from(id as string).remove(paths as string[])
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/list.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/list.ts
new file mode 100644
index 00000000000..d4dc551668b
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/list.ts
@@ -0,0 +1,32 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'POST':
+ return handlePost(req, res)
+
+ default:
+ res.setHeader('Allow', ['POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+ const { path, ...params } = req.body
+
+ const { data, error } = await supabase.storage.from(id as string).list(path, params)
+ if (error) {
+ return res.status(500).json({ error: error.message })
+ }
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/move.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/move.ts
new file mode 100644
index 00000000000..d4c8b5485f0
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/move.ts
@@ -0,0 +1,31 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'POST':
+ return handlePost(req, res)
+ default:
+ res.setHeader('Allow', ['POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+ const { from, to } = req.body
+
+ const { data, error } = await supabase.storage.from(id as string).move(from, to)
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/public-url.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/public-url.ts
new file mode 100644
index 00000000000..41efb4e8107
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/public-url.ts
@@ -0,0 +1,28 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'POST':
+ return handlePost(req, res)
+ default:
+ res.setHeader('Allow', ['POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+ const { path } = req.body
+
+ const { data } = supabase.storage.from(id as string).getPublicUrl(path)
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts
new file mode 100644
index 00000000000..8677334f21b
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts
@@ -0,0 +1,31 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'POST':
+ return handlePost(req, res)
+ default:
+ res.setHeader('Allow', ['POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id } = req.query
+ const { path, expiresIn = 60 * 60 * 24 } = req.body
+
+ const { data, error } = await supabase.storage.from(id as string).createSignedUrl(path, expiresIn)
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/api/storage/[ref]/buckets/index.ts b/studio/pages/api/storage/[ref]/buckets/index.ts
new file mode 100644
index 00000000000..e426db1810c
--- /dev/null
+++ b/studio/pages/api/storage/[ref]/buckets/index.ts
@@ -0,0 +1,42 @@
+import { createClient } from '@supabase/supabase-js'
+import apiWrapper from 'lib/api/apiWrapper'
+import { NextApiRequest, NextApiResponse } from 'next'
+
+const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'GET':
+ return handleGet(req, res)
+ case 'POST':
+ return handlePost(req, res)
+
+ default:
+ res.setHeader('Allow', ['GET', 'POST'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { data, error } = await supabase.storage.listBuckets()
+ if (error) {
+ return res.status(500).json({ error: { message: 'Internal Server Error' } })
+ }
+
+ return res.status(200).json(data)
+}
+
+const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { id, public: isPublicBucket } = req.body
+
+ const { data, error } = await supabase.storage.createBucket(id, { public: isPublicBucket })
+ if (error) {
+ return res.status(400).json({ error: { message: error.message } })
+ }
+
+ return res.status(200).json(data)
+}
diff --git a/studio/pages/project/[ref]/index.tsx b/studio/pages/project/[ref]/index.tsx
index 38d0c25de3e..b6bd9ded124 100644
--- a/studio/pages/project/[ref]/index.tsx
+++ b/studio/pages/project/[ref]/index.tsx
@@ -41,26 +41,30 @@ const Home: NextPageWithLayout = () => {
{IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE &&
}
-
-
-
Client libraries
-
-
- {CLIENT_LIBRARIES.map((library) => (
-
- ))}
-
-
-
-
-
Example projects
-
-
- {EXAMPLE_PROJECTS.sort((a, b) => a.title.localeCompare(b.title)).map((project) => (
-
- ))}
-
-
+ {project?.status !== PROJECT_STATUS.INACTIVE && (
+ <>
+
+
+
Client libraries
+
+
+ {CLIENT_LIBRARIES.map((library) => (
+
+ ))}
+
+
+
+
+
Example projects
+
+
+ {EXAMPLE_PROJECTS.sort((a, b) => a.title.localeCompare(b.title)).map((project) => (
+
+ ))}
+
+
+ >
+ )}
)
}
diff --git a/studio/stores/common/PostgresMetaInterface.ts b/studio/stores/common/PostgresMetaInterface.ts
index bd51fd319d5..c11e782498a 100644
--- a/studio/stores/common/PostgresMetaInterface.ts
+++ b/studio/stores/common/PostgresMetaInterface.ts
@@ -47,6 +47,7 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface<
state = this.STATES.INITIAL
data: { [key in DataKeys]: T } = {}
headers: any = {}
+ isInitialized: boolean = false
constructor(
rootStore: IRootStore,
@@ -68,6 +69,7 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface<
count: computed,
hasError: computed,
isLoading: computed,
+ isInitialized: observable,
load: action,
create: action,
update: action,
@@ -90,10 +92,6 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface<
return this.state === this.STATES.INITIAL || this.state === this.STATES.LOADING
}
- get isInitialized() {
- return this.state === this.STATES.LOADED || this.state === this.STATES.ERROR
- }
-
async fetchData() {
const headers = { 'Content-Type': 'application/json', ...this.headers }
const response = await get(this.url, { headers })
@@ -114,6 +112,8 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface<
console.error('Load error message', e.message)
this.setError(e)
this.setState(ERROR)
+ } finally {
+ if (!this.isInitialized) this.isInitialized = true
}
}